iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Recursion

Recursion is a function that calls itself. Every recursive solution has base cases (stop) and recursive cases (smaller problem). Powerful for trees, graphs, divide-and-conquer; risky for performance without memoisation.

Patterns, stack, tail call, alternatives

EXAMPLE
// 1) Classic — factorial
function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// 2) Fibonacci — exponential without memo
function fib(n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);     // O(2^n) — slow
}

// With memoisation — O(n)
function fibMemo(n, memo = new Map()) {
    if (n < 2) return n;
    if (memo.has(n)) return memo.get(n);
    const v = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
    memo.set(n, v);
    return v;
}

// 3) Walk a tree
class Node { constructor(v, children = []) { this.v = v; this.children = children; } }

function printTree(node, indent = 0) {
    console.log(' '.repeat(indent) + node.v);
    for (const c of node.children) printTree(c, indent + 2);
}

// Sum values in a tree
function sum(node) {
    return node.v + node.children.reduce((s, c) => s + sum(c), 0);
}

// 4) Binary tree traversals
function inorder(node, visit) {
    if (!node) return;
    inorder(node.left, visit);
    visit(node.v);
    inorder(node.right, visit);
}

function preorder(node, visit) {
    if (!node) return;
    visit(node.v);
    preorder(node.left, visit);
    preorder(node.right, visit);
}

function postorder(node, visit) {
    if (!node) return;
    postorder(node.left, visit);
    postorder(node.right, visit);
    visit(node.v);
}

// 5) Tree depth
function depth(node) {
    if (!node) return 0;
    return 1 + Math.max(depth(node.left), depth(node.right));
}

// 6) Divide and conquer — merge sort
function mergeSort(arr) {
    if (arr.length <= 1) return arr;
    const mid = arr.length >> 1;
    const left  = mergeSort(arr.slice(0, mid));
    const right = mergeSort(arr.slice(mid));
    return merge(left, right);
}

function merge(a, b) {
    const out = [];
    let i = 0, j = 0;
    while (i < a.length && j < b.length) {
        if (a[i] <= b[j]) out.push(a[i++]);
        else              out.push(b[j++]);
    }
    return [...out, ...a.slice(i), ...b.slice(j)];
}

// 7) Quicksort
function quickSort(arr) {
    if (arr.length <= 1) return arr;
    const pivot = arr[0];
    const less    = arr.slice(1).filter(x => x <  pivot);
    const equal   = arr.filter   (x => x === pivot);
    const greater = arr.slice(1).filter(x => x >  pivot);
    return [...quickSort(less), ...equal, ...quickSort(greater)];
}

// 8) Backtracking — generate all subsets
function subsets(nums) {
    const out = [];
    function recurse(i, current) {
        if (i === nums.length) { out.push([...current]); return; }
        recurse(i + 1, current);                    // skip
        current.push(nums[i]);
        recurse(i + 1, current);                    // include
        current.pop();
    }
    recurse(0, []);
    return out;
}

// 9) Backtracking — permutations
function permutations(nums) {
    const out = [];
    function recurse(current, used) {
        if (current.length === nums.length) { out.push([...current]); return; }
        for (let i = 0; i < nums.length; i++) {
            if (used[i]) continue;
            used[i] = true;
            current.push(nums[i]);
            recurse(current, used);
            current.pop();
            used[i] = false;
        }
    }
    recurse([], new Array(nums.length).fill(false));
    return out;
}

// 10) N-queens
function nQueens(n) {
    const out = [];
    const cols = new Set(), d1 = new Set(), d2 = new Set();
    const board = Array.from({ length: n }, () => Array(n).fill('.'));
    function recurse(row) {
        if (row === n) {
            out.push(board.map(r => r.join('')));
            return;
        }
        for (let c = 0; c < n; c++) {
            if (cols.has(c) || d1.has(row + c) || d2.has(row - c)) continue;
            cols.add(c); d1.add(row + c); d2.add(row - c);
            board[row][c] = 'Q';
            recurse(row + 1);
            board[row][c] = '.';
            cols.delete(c); d1.delete(row + c); d2.delete(row - c);
        }
    }
    recurse(0);
    return out;
}

// 11) Graph DFS
function dfs(graph, start) {
    const visited = new Set();
    function recurse(node) {
        if (visited.has(node)) return;
        visited.add(node);
        console.log(node);
        for (const n of graph[node] ?? []) recurse(n);
    }
    recurse(start);
}

// 12) Recursion vs iteration
// Iteration is generally faster + uses less stack.
// Recursion is clearer for: tree traversal, divide-and-conquer, backtracking.
// Stack overflow is real — JS default limit ~10k frames. Don't recurse on million-item arrays.

// 13) Tail recursion (PSA: JS doesn't optimise it; many languages do)
// Tail call: the recursive call is the LAST thing the function does.
function factorialTail(n, acc = 1) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);    // tail call (in TCO languages, no extra stack frame)
}
// Languages with TCO: Scheme, Erlang, Elixir, Scala (partial), Lua, OCaml.
// Without TCO, you still risk stack overflow.

// 14) Convert recursion → iteration with a stack
function iterDfs(node) {
    const stack = [node];
    while (stack.length) {
        const n = stack.pop();
        if (!n) continue;
        console.log(n.v);
        stack.push(n.right, n.left);          // push right first so left processes first
    }
}

// 15) When to use recursion
// - Natural fit: tree / graph / divide-and-conquer / backtracking
// - Problem state collapses to a smaller version of itself
// - Memoise overlapping subproblems (DP)
//
// When to convert to iteration
// - Deep recursion (millions of frames) → stack overflow
// - Hot path performance — iteration is usually faster
// - You see overlapping subproblems → memoise OR build a table bottom-up

// 16) Common pitfalls
//   • Forgetting the base case → infinite recursion → stack overflow
//   • Mutating shared state across calls without proper backtracking
//   • Recomputing the same subproblems → exponential time (memoise!)
//   • Deep recursion in async code → 'too much recursion' errors
//   • Trampolining can help in JS: convert tail-call recursion into a while loop returning closures

Why it matters

Recursion is unbeatable for trees, graphs, and divide-and-conquer. Memoise when subproblems overlap; convert to a stack-based loop when depth could blow the call stack. Most “recursive” problems have an iterative twin.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
function fact(n) { return n <= 1 ? 1 : n * fact(n - 1); }
// Use a stack or trampoline if you might overflow.
Try it Yourself »

Discussion

Loading…