Overview
Recursion is when a function solves a problem by calling itself on a smaller version of the same problem. Every recursive function has two parts: a base case that returns an answer directly, and a recursive case that reduces the problem and trusts the recursive call to solve the smaller piece.
Recursion is the foundation for tree and graph traversal, backtracking, divide and conquer, and dynamic programming. The key mental shift is to stop tracing every call and instead trust the function's definition: if sum(arr, i) returns the sum from index i onward, then sum(arr, 0) = arr[0] + sum(arr, 1).
To count the dolls, open the outer one and ask the same question about the doll inside: 1 + count(inner). When you reach the smallest solid doll (the base case), you answer 1 and all the waiting answers add up on the way back out.
When to use it
- The data is recursive: trees, nested lists, file systems, JSON.
- The problem is naturally defined in terms of smaller versions of itself (factorial, Fibonacci, power).
- You need to explore choices: subsets, permutations, paths (backtracking).
- You can split the input into independent halves (merge sort, quicksort).
Problem patterns it solves
Recognize it when: process one element, recurse on the rest: reverse, sum, count, check palindrome.
- 344. Reverse String
- 206. Reverse Linked List (recursive)
- 234. Palindrome Linked List
Recognize it when: answer depends on left and right subproblems.
- 104. Maximum Depth of Binary Tree
- 226. Invert Binary Tree
- 509. Fibonacci Number
Recognize it when: for each element choose to include it or skip it: subsets, subsequences, knapsack.
- 78. Subsets
- 39. Combination Sum
- 416. Partition Equal Subset Sum
Recognize it when: split into halves, solve each, combine.
- 50. Pow(x, n)
- 912. Sort an Array
- 241. Different Ways to Add Parentheses
Recognize it when: build answers character by character with constraints.
- 22. Generate Parentheses
- 17. Letter Combinations of a Phone Number
- 1190. Reverse Substrings Between Each Pair of Parentheses
Where it is used in real software
Computing a folder's total size, or deleting a directory tree (rm -r), recursively processes each subfolder.
Recursive descent parsers handle nested expressions like (1 + (2 * 3)). JSON.parse and most expression evaluators work this way.
React renders a component tree recursively; DOM traversal APIs and tree-shaped menus are processed with recursion.
Fractal rendering, quadtrees for collision detection, and scene graphs in game engines are recursive structures.
Key terms
- Base case
- The smallest input answered directly without another call. Without it, recursion never stops.
- Recursive case
- The part that reduces the problem and calls the function again.
- Call stack
- The runtime stack of active function calls. Each call waits for its child to return.
- Stack overflow
- Error when recursion depth exceeds the stack limit (often around 10,000 frames in JS).
- Leap of faith
- Assume the recursive call correctly solves the smaller problem; only verify how you use its result.
How to write any recursive function
- 1Define the function in one sentence
Example: sum(arr, i) returns the sum of arr[i..end]. Be precise about inputs and what is returned.
- 2Find the base case
What is the smallest input? When i === arr.length, the remaining sum is 0.
- 3Make the problem smaller
Reduce toward the base case: i + 1, n - 1, n / 2, node.left.
- 4Combine using the definition
Trust sum(arr, i + 1) and use it: arr[i] + sum(arr, i + 1).
- 5Check progress and termination
Every call must move closer to the base case, or it will loop forever.
STEP 1Call fact(4). It needs fact(3) before it can return, so it waits on the stack.
Trace of fibonacci(4) without memoization
fib(n) = fib(n - 1) + fib(n - 2), fib(0) = 0, fib(1) = 1
| Call | Depth | Calls it makes | Returns |
|---|---|---|---|
| fib(4) | 0 | fib(3), fib(2) | 3 |
| fib(3) | 1 | fib(2), fib(1) | 2 |
| fib(2) | 2 | fib(1), fib(0) | 1 |
| fib(1), fib(0) | 3 | base cases | 1, 0 |
| fib(1) | 2 | base case | 1 |
| fib(2) again | 1 | fib(1), fib(0) again | 1 |
NOWCall: fib(4) | Depth: 0 | Calls it makes: fib(3), fib(2) | Returns: 3
fib(2) is computed twice already at n = 4, and the duplication doubles with each level, giving O(2^n) calls. Caching results (memoization) reduces this to O(n) and is the bridge to dynamic programming.
Implementation
// Linear recursionfunction factorial(n) { if (n <= 1) return 1; // base case return n * factorial(n - 1); // smaller problem} // Recursion on an index instead of slicing (slice would copy: O(n^2))function isPalindrome(s, left = 0, right = s.length - 1) { if (left >= right) return true; if (s[left] !== s[right]) return false; return isPalindrome(s, left + 1, right - 1);} // Divide in half: O(log n) callsfunction power(x, n) { if (n === 0) return 1; if (n < 0) return 1 / power(x, -n); const half = power(x, Math.floor(n / 2)); return n % 2 === 0 ? half * half : half * half * x;} // Tree recursion with memoization: O(2^n) becomes O(n)function fib(n, memo = new Map()) { if (n <= 1) return n; if (memo.has(n)) return memo.get(n); const value = fib(n - 1, memo) + fib(n - 2, memo); memo.set(n, value); return value;} // Generate strings with constraintsfunction generateParenthesis(n) { const result = []; function build(current, open, close) { if (current.length === 2 * n) { result.push(current); return; } if (open < n) build(current + "(", open + 1, close); if (close < open) build(current + ")", open, close + 1); } build("", 0, 0); return result;}Complexity and performance
One call per element; O(n) stack space.
Depth log n; O(log n) stack space.
Like naive Fibonacci; memoize if states repeat.
Not O(total calls): only one path is active at a time.
Trade-offs
Recursive tree code is short and clear, but a skewed tree with 100,000 nodes can overflow the stack. Convert to an explicit stack if depth can be large.
Passing arr.slice(1) or s.substring(1) creates a copy each call, turning O(n) into O(n^2). Pass indexes instead.
If the same arguments appear multiple times in the recursion tree, add memoization.
Variants and related techniques
Head recursion does work after the recursive call returns (printing in reverse); tail recursion does work before (printing in order).
A public function often wraps a private helper that carries extra state such as an index, a path, or a result list.
Two functions call each other, as in isEven/isOdd or grammar rules in parsers.
Common mistakes
- Missing or wrong base case.
Fix: Test the smallest inputs first: empty array, n = 0, null node.
- Not returning the recursive result.
Fix: Write return f(n - 1) rather than just calling f(n - 1).
- Sharing a mutable list without undoing changes.
Fix: Push before the call and pop after it (backtracking), or pass a copy when recording a result.
- Tracing every call in your head.
Fix: Trust the definition for the smaller problem and verify only the current level.
Interview questions
What happens in memory during recursion?
Each call pushes a frame with its parameters and local variables onto the call stack. It stays there until the call returns, so memory is proportional to the maximum depth.
Can every recursive function be written iteratively?
Yes. Any recursion can be simulated with an explicit stack. Tail-recursive functions convert to a simple loop.
How do you decide recursion is the right tool?
When the problem breaks into smaller identical subproblems, or the data is hierarchical. If the recursion revisits the same states, it becomes dynamic programming.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| 509. Fibonacci Number | Easy | Naive recursion, then memoize. |
| 344. Reverse String | Easy | Two-index recursion. |
| 206. Reverse Linked List | Easy | Recursive pointer rewiring. |
| 50. Pow(x, n) | Medium | Halving recursion and negative exponents. |
| 22. Generate Parentheses | Medium | Recursion with constraints. |
| 1823. Find the Winner of the Circular Game | Medium | Josephus recurrence: f(n) = (f(n - 1) + k) % n. |