Recursion
One-line summary: Solve a problem by breaking it into smaller instances of itself โ the foundation of DFS, divide-and-conquer, backtracking, and tree algorithms.
Diagramโ
๐ฏ Conceptโ
Recursion is when a function calls itself to solve smaller instances of the same problem. Every recursive function needs:
- Base case โ the smallest input, solved directly; stops the recursion.
- Recursive case โ reduces the problem toward the base case.
- Trust the recursion โ assume the recursive call returns the correct answer (the "recursive leap of faith").
The Call Stackโ
Each recursive call is pushed onto the call stack with its own local variables. When the base case returns, the frames unwind in reverse (LIFO) order.
flowchart TD
A["fact(4)"] --> B["4 * fact(3)"]
B --> C["3 * fact(2)"]
C --> D["2 * fact(1)"]
D --> E["fact(1) = 1 (base case)"]
E -.returns 1.-> D
D -.returns 2.-> C
C -.returns 6.-> B
B -.returns 24.-> A
Recursion vs Iterationโ
| Aspect | Recursion | Iteration |
|---|---|---|
| Readability | Elegant for trees/divide-and-conquer | Simpler for linear loops |
| Memory | O(depth) stack frames | O(1) usually |
| Risk | Stack overflow on deep recursion | None |
| Conversion | Any recursion โ loop + explicit stack | โ |
Memoization (Intro)โ
When recursive calls repeat the same subproblem, cache results to avoid recomputation โ turning exponential time into linear. This is the bridge to Dynamic Programming.
โก Time & Space Complexityโ
| Recursive Pattern | Time | Space (stack) |
|---|---|---|
| Linear (factorial) | O(n) | O(n) |
| Binary (naive Fibonacci) | O(2โฟ) | O(n) |
| Binary (memoized Fibonacci) | O(n) | O(n) |
| Divide & conquer (merge sort) | O(n log n) | O(log n) |
| Fast exponentiation | O(log n) | O(log n) |
Key Insight: Recursion depth determines auxiliary space โ always bound the depth to avoid stack overflow.
Common Patternsโ
Factorialโ
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Fibonacci (memoised)โ
function fib(n, memo = {}) {
if (n <= 1) return n;
if (memo[n]) return memo[n];
return (memo[n] = fib(n - 1, memo) + fib(n - 2, memo));
}
Power (fast exponentiation)โ
function power(base, exp) {
if (exp === 0) return 1;
if (exp % 2 === 0) {
const half = power(base, exp / 2);
return half * half;
}
return base * power(base, exp - 1);
}
Pitfallsโ
- Missing base case โ infinite recursion / stack overflow
- Redundant subproblem calls โ add memoisation
- Deep recursion in JS โ stack size ~10k; consider iterative with explicit stack
๐งช Worked Example: Naive vs Memoized Fibonacciโ
The classic demonstration of why memoization matters.
// โ Naive: recomputes the same subproblems exponentially
function fibNaive(n) {
if (n <= 1) return n;
return fibNaive(n - 1) + fibNaive(n - 2);
}
// Time: O(2^n) โ fib(40) makes ~1.6 billion calls!
// โ
Memoized: each subproblem solved once
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;
}
// Time: O(n), Space: O(n)
Takeaway: The recursion tree for fibNaive(5) computes fib(2) three times and fib(1) five times. Memoization prunes those repeats โ the same idea that powers Dynamic Programming.
Practice Problemsโ
Related Topicsโ
- Backtracking โ recursion with undo
- Dynamic Programming โ memoised recursion
- Trees โ most tree algorithms are recursive
โ Back to Home ยท ยฉ sparshjaswal