Recursion Patterns
1. Tree Recursion Patterns
| Pattern | Structure | Complexity | Application |
|---|---|---|---|
| Single Branch | One recursive call per invocation | O(n) linear |
Linked list traversal, linear search |
| Binary Tree | Two recursive calls (left, right) | O(2^h) exponential |
Binary tree traversal, Fibonacci naive |
| N-ary Tree | N recursive calls for N children | O(N^h) |
Directory traversal, game trees |
| Pruned Tree | Conditional recursion with early exit | Better than worst case | Backtracking, search with constraints |
Example: Binary tree recursion
// Preorder traversal
function preorder(root) {
if (!root) return;
console.log(root.val); // Process root
preorder(root.left); // Recurse left
preorder(root.right); // Recurse right
}
// Inorder traversal
function inorder(root) {
if (!root) return;
inorder(root.left);
console.log(root.val);
inorder(root.right);
}
// Postorder traversal
function postorder(root) {
if (!root) return;
postorder(root.left);
postorder(root.right);
console.log(root.val);
}
// Time: O(n), Space: O(h) recursion stack
Example: N-ary tree recursion
function traverseNary(root) {
if (!root) return;
// Process current node
console.log(root.val);
// Recurse on all children
for (const child of root.children) {
traverseNary(child);
}
}
// Count nodes in N-ary tree
function countNodes(root) {
if (!root) return 0;
let count = 1; // Current node
for (const child of root.children) {
count += countNodes(child);
}
return count;
}
// Time: O(n), Space: O(h)
Example: Tree recursion with return value
// Find maximum value in binary tree
function findMax(root) {
if (!root) return -Infinity;
const leftMax = findMax(root.left);
const rightMax = findMax(root.right);
return Math.max(root.val, leftMax, rightMax);
}
// Check if tree is symmetric
function isSymmetric(root) {
function isMirror(left, right) {
if (!left && !right) return true;
if (!left || !right) return false;
return left.val === right.val &&
isMirror(left.left, right.right) &&
isMirror(left.right, right.left);
}
return !root || isMirror(root.left, root.right);
}
// Path sum exists
function hasPathSum(root, targetSum) {
if (!root) return false;
// Leaf node
if (!root.left && !root.right) {
return root.val === targetSum;
}
return hasPathSum(root.left, targetSum - root.val) ||
hasPathSum(root.right, targetSum - root.val);
}
2. Tail Recursion Optimization
| Concept | Definition | Benefit | Limitation |
|---|---|---|---|
| Tail Call | Recursive call is last operation | Can reuse stack frame | No pending operations after recursion |
| TCO (Tail Call Optimization) | Compiler optimization to O(1) space | Converts recursion to iteration | Not guaranteed in JavaScript |
| Accumulator Pattern | Pass result as parameter | Enables tail recursion | Requires function redesign |
| Manual Conversion | Rewrite as while loop | Guaranteed O(1) space | More verbose code |
Example: Non-tail recursive factorial
function factorial(n) {
if (n <= 1) return 1;
// NOT tail recursive
// Pending operation: multiply result by n
return n * factorial(n - 1);
}
// Stack frames:
// factorial(5)
// 5 * factorial(4)
// 4 * factorial(3)
// 3 * factorial(2)
// 2 * factorial(1)
// return 1
// return 2 * 1 = 2
// return 3 * 2 = 6
// return 4 * 6 = 24
// return 5 * 24 = 120
// Space: O(n) stack frames
Example: Tail recursive factorial
function factorialTail(n, acc = 1) {
if (n <= 1) return acc;
// Tail recursive
// No pending operations after recursion
return factorialTail(n - 1, n * acc);
}
// With TCO optimization:
// factorialTail(5, 1)
// factorialTail(4, 5) // reuse frame
// factorialTail(3, 20) // reuse frame
// factorialTail(2, 60) // reuse frame
// factorialTail(1, 120) // reuse frame
// return 120
// Space: O(1) with TCO
// Without TCO: still O(n) in JavaScript
Example: Converting to iterative (guaranteed O(1) space)
// Tail recursive Fibonacci
function fibTail(n, a = 0, b = 1) {
if (n === 0) return a;
if (n === 1) return b;
return fibTail(n - 1, b, a + b);
}
// Iterative version (manual tail call elimination)
function fibIterative(n) {
if (n === 0) return 0;
if (n === 1) return 1;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
// Both O(n) time, iterative guaranteed O(1) space
// fibTail depends on TCO support
// Sum of array - tail recursive
function sumTail(arr, index = 0, acc = 0) {
if (index === arr.length) return acc;
return sumTail(arr, index + 1, acc + arr[index]);
}
// Iterative equivalent
function sumIterative(arr) {
let sum = 0;
for (const num of arr) {
sum += num;
}
return sum;
}
3. Recursion Tree Complexity Analysis
| Example | Recursion Tree | Work Per Level | Total Complexity |
|---|---|---|---|
| Linear (Factorial) | Single branch, depth n | O(1) |
O(n) |
| Binary (Fibonacci naive) | 2 children, depth n | O(2^level) |
O(2^n) |
| Divide & Conquer (Merge Sort) | 2 children, depth log n | O(n) |
O(n log n) |
| Master Theorem | T(n) = aT(n/b) + f(n) | Depends on f(n) vs n^(log_b a) | Three cases determine result |
Example: Fibonacci recursion tree analysis
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
/*
Recursion tree for fib(5):
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) ... ...
Level 0: 1 node = 2^0
Level 1: 2 nodes = 2^1
Level 2: 4 nodes = 2^2
...
Level k: 2^k nodes (approximately)
Depth ≈ n
Total nodes ≈ 2^n
Time: O(2^n), Space: O(n) stack depth
*/
// Each fib(k) computed multiple times!
// fib(3) computed twice, fib(2) three times, etc.
// Overlapping subproblems -> use memoization!
Example: Master Theorem applications
// Binary Search: T(n) = T(n/2) + O(1)
// a=1, b=2, f(n)=O(1)
// log_b(a) = log_2(1) = 0
// f(n) = O(n^0) = O(1)
// Case 2: f(n) = Θ(n^log_b(a))
// Result: O(log n)
// Merge Sort: T(n) = 2T(n/2) + O(n)
// a=2, b=2, f(n)=O(n)
// log_b(a) = log_2(2) = 1
// f(n) = O(n^1) = O(n)
// Case 2: f(n) = Θ(n^log_b(a))
// Result: O(n log n)
// Karatsuba: T(n) = 3T(n/2) + O(n)
// a=3, b=2, f(n)=O(n)
// log_b(a) = log_2(3) ≈ 1.585
// f(n) = O(n^1) < O(n^1.585)
// Case 1: f(n) = O(n^c) where c < log_b(a)
// Result: O(n^log_2(3)) ≈ O(n^1.585)
// Strassen Matrix: T(n) = 7T(n/2) + O(n^2)
// a=7, b=2, f(n)=O(n^2)
// log_b(a) = log_2(7) ≈ 2.807
// f(n) = O(n^2) < O(n^2.807)
// Case 1: Result: O(n^log_2(7)) ≈ O(n^2.807)
4. Memoization (Top-Down DP)
| Concept | Mechanism | Benefit | Trade-off |
|---|---|---|---|
| Caching Results | Store computed values in map/array | Avoid redundant computations | Extra space for cache |
| Top-Down Approach | Start from problem, recurse to base | Natural recursive thinking | Recursion stack overhead |
| Lazy Computation | Only compute needed subproblems | May compute fewer states than bottom-up | Function call overhead |
| When to Use | Overlapping subproblems exist | Convert O(2^n) to O(n) or O(n²) | Need to identify subproblem structure |
Example: Fibonacci with memoization
function fibMemo(n, memo = {}) {
// Check cache
if (n in memo) return memo[n];
// Base cases
if (n <= 1) return n;
// Compute and cache
memo[n] = fibMemo(n - 1, memo) +
fibMemo(n - 2, memo);
return memo[n];
}
// Alternatively with array
function fibMemoArray(n) {
const memo = Array(n + 1).fill(-1);
function helper(n) {
if (memo[n] !== -1) return memo[n];
if (n <= 1) return n;
memo[n] = helper(n - 1) + helper(n - 2);
return memo[n];
}
return helper(n);
}
// Time: O(n), Space: O(n)
// Each subproblem computed once!
Example: Climbing stairs memoized
function climbStairs(n) {
const memo = {};
function climb(n) {
if (n in memo) return memo[n];
if (n <= 2) return n;
memo[n] = climb(n - 1) + climb(n - 2);
return memo[n];
}
return climb(n);
}
// Time: O(n), Space: O(n)
// Without memoization: O(2^n)
// Can take 1 or 2 steps at a time
// How many ways to reach step n?
// climb(n) = climb(n-1) + climb(n-2)
Example: Longest increasing subsequence (LIS) memoized
function lengthOfLIS(nums) {
const memo = {};
function lis(index, prev) {
const key = `${index},${prev}`;
if (key in memo) return memo[key];
if (index === nums.length) return 0;
// Option 1: Skip current element
let skip = lis(index + 1, prev);
// Option 2: Take current if valid
let take = 0;
if (prev === -1 || nums[index] > nums[prev]) {
take = 1 + lis(index + 1, index);
}
memo[key] = Math.max(skip, take);
return memo[key];
}
return lis(0, -1);
}
// [10,9,2,5,3,7,101,18] -> 4 ([2,3,7,101])
// Time: O(n²), Space: O(n²)
// Alternative: O(n) space with index-only memoization
function lengthOfLISOptimized(nums) {
const memo = Array(nums.length).fill(-1);
function lis(index) {
if (memo[index] !== -1) return memo[index];
let maxLen = 1;
for (let j = 0; j < index; j++) {
if (nums[j] < nums[index]) {
maxLen = Math.max(maxLen, 1 + lis(j));
}
}
memo[index] = maxLen;
return maxLen;
}
let result = 0;
for (let i = 0; i < nums.length; i++) {
result = Math.max(result, lis(i));
}
return result;
}
5. Recursive Data Structures
| Structure | Definition | Operations | Complexity |
|---|---|---|---|
| Linked List | Node + pointer to next node | Traverse, insert, delete recursively | O(n) time, O(n) space for recursion |
| Binary Tree | Node + left/right child pointers | All tree operations naturally recursive | O(n) time, O(h) space |
| Trie | Node + map of children | Search, insert, delete with recursion | O(L) where L = word length |
| Graph | Nodes + edges (adjacency list/matrix) | DFS naturally recursive | O(V + E) |
Example: Recursive linked list operations
class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
// Recursive traversal
function printList(head) {
if (!head) return;
console.log(head.val);
printList(head.next);
}
// Recursive reverse
function reverseList(head) {
if (!head || !head.next) return head;
const newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
// Recursive search
function search(head, target) {
if (!head) return false;
if (head.val === target) return true;
return search(head.next, target);
}
// Recursive insert at end
function insertEnd(head, val) {
if (!head) return new ListNode(val);
head.next = insertEnd(head.next, val);
return head;
}
Example: Recursive tree construction
class TreeNode {
constructor(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
// Build tree from inorder and preorder
function buildTree(preorder, inorder) {
if (preorder.length === 0) return null;
const rootVal = preorder[0];
const root = new TreeNode(rootVal);
const mid = inorder.indexOf(rootVal);
root.left = buildTree(
preorder.slice(1, mid + 1),
inorder.slice(0, mid)
);
root.right = buildTree(
preorder.slice(mid + 1),
inorder.slice(mid + 1)
);
return root;
}
// Serialize tree
function serialize(root) {
if (!root) return 'null';
return root.val + ',' +
serialize(root.left) + ',' +
serialize(root.right);
}
// Deserialize tree
function deserialize(data) {
const values = data.split(',');
let index = 0;
function build() {
if (values[index] === 'null') {
index++;
return null;
}
const node = new TreeNode(parseInt(values[index]));
index++;
node.left = build();
node.right = build();
return node;
}
return build();
}
6. Mutual Recursion Patterns
| Pattern | Description | Example | Use Case |
|---|---|---|---|
| Two Functions | Function A calls B, B calls A | Even/odd checker, state machines | Alternating states |
| Multiple Functions | Cycle of 3+ functions | Parsing, expression evaluation | Complex state transitions |
| Base Cases | Each function needs termination | Critical for avoiding infinite loops | Careful design required |
| Conversion | Can convert to single function with state | Add parameter to track which "phase" | May simplify or complicate |
Example: Even/odd mutual recursion
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n) {
if (n === 0) return false;
return isEven(n - 1);
}
// isEven(4) -> isOdd(3) -> isEven(2) -> isOdd(1) -> isEven(0) -> true
// Demonstrates mutual recursion, though inefficient
// Better: return n % 2 === 0
Example: Expression parser mutual recursion
// Grammar:
// expr -> term (('+' | '-') term)*
// term -> factor (('*' | '/') factor)*
// factor -> number | '(' expr ')'
class Parser {
constructor(tokens) {
this.tokens = tokens;
this.pos = 0;
}
parseExpr() {
let result = this.parseTerm();
while (this.pos < this.tokens.length &&
(this.tokens[this.pos] === '+' ||
this.tokens[this.pos] === '-')) {
const op = this.tokens[this.pos++];
const right = this.parseTerm();
result = op === '+' ? result + right : result - right;
}
return result;
}
parseTerm() {
let result = this.parseFactor();
while (this.pos < this.tokens.length &&
(this.tokens[this.pos] === '*' ||
this.tokens[this.pos] === '/')) {
const op = this.tokens[this.pos++];
const right = this.parseFactor();
result = op === '*' ? result * right : result / right;
}
return result;
}
parseFactor() {
if (this.tokens[this.pos] === '(') {
this.pos++; // Skip '('
const result = this.parseExpr(); // Mutual recursion!
this.pos++; // Skip ')'
return result;
}
return parseInt(this.tokens[this.pos++]);
}
}
// const parser = new Parser(['(', '2', '+', '3', ')', '*', '4']);
// parser.parseExpr() -> 20
// parseExpr calls parseTerm, parseTerm calls parseFactor,
// parseFactor calls parseExpr for nested expressions
Example: State machine mutual recursion
// Simple state machine: accepting strings with alternating a's and b's
function stateA(str, index) {
if (index === str.length) return true; // Accept
if (str[index] === 'a') {
return stateB(str, index + 1); // Transition to B
}
return false; // Reject
}
function stateB(str, index) {
if (index === str.length) return false; // Reject (must end in A)
if (str[index] === 'b') {
return stateA(str, index + 1); // Transition to A
}
return false; // Reject
}
function accepts(str) {
if (str.length === 0) return false;
return stateA(str, 0);
}
// accepts("ab") -> true
// accepts("abab") -> true
// accepts("aba") -> false
// accepts("abba") -> false
// Mutual recursion models state transitions
Recursion Patterns Summary
- Tree Recursion: Binary/N-ary recursive calls, exponential growth without optimization
- Tail Recursion: Last operation is recursive call, enables O(1) space with TCO
- Recursion Tree Analysis: Visualize calls, count nodes per level, apply Master Theorem
- Memoization: Cache results to convert O(2^n) to O(n) or O(n²)
- Recursive Structures: Linked lists, trees, graphs naturally recursive
- Mutual Recursion: Functions call each other, useful for state machines and parsers
Note: Memoization is critical for recursive problems with overlapping subproblems. Fibonacci naive: O(2^n), with memoization: O(n). Tail recursion optimization converts recursion to iteration internally, achieving O(1) space, but JavaScript engines may not guarantee TCO. When in doubt, manually convert to iteration.
Warning: Deep recursion can cause stack overflow. JavaScript typically allows ~10,000-50,000 frames. For very deep recursion, use iteration or trampolining. Memoization requires extra space - be mindful of memory constraints. In mutual recursion, ensure all functions have proper base cases to avoid infinite loops.