Recursion Patterns Cheatsheet
Core Concepts
1. Base Cases
function factorial(n) {
// Base case
if (n <= 1) return 1;
// Recursive case
return n * factorial(n - 1);
}
2. Recursive Structure
function recursiveFunction(input) {
// 1. Base cases
if (baseCase) return baseValue;
// 2. Process current level
let current = processCurrentLevel(input);
// 3. Recursive call with smaller input
let subResult = recursiveFunction(smallerInput);
// 4. Combine results
return combineResults(current, subResult);
}
Common Patterns
1. Linear Recursion
Direct recursive call with smaller input.
// Sum array elements
function sum(arr, index = 0) {
if (index === arr.length) return 0;
return arr[index] + sum(arr, index + 1);
}
// String reversal
function reverse(str) {
if (str.length <= 1) return str;
return reverse(str.slice(1)) + str[0];
}
2. Binary Recursion
Two recursive calls in each function.
// Fibonacci sequence
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Binary tree traversal
function traverse(node) {
if (!node) return;
traverse(node.left);
traverse(node.right);
}
3. Tail Recursion
Recursive call is the last operation.
// Factorial with tail recursion
function factorialTail(n, acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
// Sum with tail recursion
function sumTail(arr, index = 0, acc = 0) {
if (index === arr.length) return acc;
return sumTail(arr, index + 1, acc + arr[index]);
}
4. Multiple Recursion
Multiple recursive calls at different positions.
// Generate all permutations
function permutations(arr) {
if (arr.length <= 1) return [arr];
const result = [];
for (let i = 0; i < arr.length; i++) {
const current = arr[i];
const remaining = [...arr.slice(0, i), ...arr.slice(i + 1)];
const perms = permutations(remaining);
for (let perm of perms) {
result.push([current, ...perm]);
}
}
return result;
}
// Generate all subsets
function subsets(arr) {
if (arr.length === 0) return [[]];
const first = arr[0];
const rest = arr.slice(1);
const subsWithoutFirst = subsets(rest);
const subsWithFirst = subsWithoutFirst.map(sub => [first, ...sub]);
return [...subsWithoutFirst, ...subsWithFirst];
}
5. Backtracking
Try different paths and undo if not valid.
// N-Queens Problem
function solveNQueens(n) {
const board = Array(n).fill().map(() => Array(n).fill('.'));
const result = [];
function isValid(row, col) {
// Check column
for (let i = 0; i < row; i++) {
if (board[i][col] === 'Q') return false;
}
// Check diagonal
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 'Q') return false;
}
// Check anti-diagonal
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 'Q') return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
result.push(board.map(row => row.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (isValid(row, col)) {
board[row][col] = 'Q';
backtrack(row + 1);
board[row][col] = '.';
}
}
}
backtrack(0);
return result;
}
6. Divide and Conquer
Split problem into smaller subproblems.
// Merge Sort
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return [...result, ...left.slice(i), ...right.slice(j)];
}
Optimization Techniques
1. Memoization
Cache results to avoid redundant calculations.
// Fibonacci with memoization
function fibMemo(n, memo = new Map()) {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n);
const result = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
memo.set(n, result);
return result;
}
2. Path Recording
Keep track of path during recursion.
// Find all paths in binary tree
function findPaths(root) {
const paths = [];
function dfs(node, path) {
if (!node) return;
path.push(node.val);
if (!node.left && !node.right) {
paths.push([...path]);
}
dfs(node.left, path);
dfs(node.right, path);
path.pop();
}
dfs(root, []);
return paths;
}
3. State Management
Pass state through recursive calls.
// Tree max path sum
function maxPathSum(root) {
let maxSum = -Infinity;
function dfs(node) {
if (!node) return 0;
const left = Math.max(0, dfs(node.left));
const right = Math.max(0, dfs(node.right));
maxSum = Math.max(maxSum, node.val + left + right);
return node.val + Math.max(left, right);
}
dfs(root);
return maxSum;
}
Common Pitfalls and Solutions
1. Stack Overflow
// Problem
function infinity(n) {
return infinity(n + 1); // Will cause stack overflow
}
// Solution: Add base case
function safe(n, limit = 1000) {
if (n >= limit) return n;
return safe(n + 1, limit);
}
2. Redundant Calculations
// Problem
function slowFib(n) {
if (n <= 1) return n;
return slowFib(n - 1) + slowFib(n - 2); // Many redundant calls
}
// Solution: Use memoization
const fastFib = (n, memo = new Map()) => {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n);
const result = fastFib(n - 1, memo) + fastFib(n - 2, memo);
memo.set(n, result);
return result;
};
3. Incorrect Base Case
// Problem
function factorial(n) {
if (n == 0) return 0; // Wrong base case
return n * factorial(n - 1);
}
// Solution
function factorial(n) {
if (n <= 1) return 1; // Correct base case
return n * factorial(n - 1);
}
Testing Recursive Functions
// Unit tests for recursive functions
function testRecursion() {
// Test base cases
console.assert(factorial(0) === 1, "factorial(0) should be 1");
console.assert(factorial(1) === 1, "factorial(1) should be 1");
// Test recursive cases
console.assert(factorial(5) === 120, "factorial(5) should be 120");
// Test edge cases
console.assert(factorial(-1) === 1, "factorial(-1) should be 1");
}