Recursion basics
Recursion is a function calling itself after reducing the problem to a smaller instance of the same problem. Every correct recursive function needs two parts:
- A base case — the simplest case where recursion stops (
n == 0,node == null, an empty list). Without a base case, or if it is never reached, the result is a stack overflow. - A recursive case — shrink the problem and call itself. Every call must move the input closer to the base case.
A useful mental model is the "leap of faith": assume the function already works correctly for n-1; your only job is to build the answer for n out of that result. Trying to picture the entire call tree in your head is the most common source of confusion.
Recursion fits naturally in: tree and graph traversal, nested structures (JSON, file systems, the DOM), divide-and-conquer algorithms (merge sort, quick sort, binary search) and backtracking (permutations, sudoku, N-Queens).
How the call stack grows. Each recursive call creates a new frame, and that frame lives until the function returns. During factorial(5), five frames sit on the stack simultaneously; as the deepest call returns, the frames unwind one by one and results propagate upward.
Two consequences follow:
- Recursion's space complexity equals its depth:
O(d).O(n)forfactorial(n),O(log n)for DFS on a balanced tree,O(n)on a skewed one. - The stack is bounded (a few MB; roughly 10,000 frames deep in JS). If depth depends on user input, relying on recursion is risky.
Tail recursion means the recursive call is the last operation in the function (return helper(n - 1, acc * n), not return n * helper(n - 1)). Some languages turn that into a loop and keep the stack constant: Kotlin does with the tailrec keyword. Most JS runtimes and Dart do not implement tail-call optimisation — do not count on it.
| Criterion | Recursion | Iteration |
|---|---|---|
| Readability (trees, nested data) | Very high — the code mirrors the shape of the problem | Lower — you must maintain a stack by hand |
| Readability (simple loop) | Feels artificial | Natural |
| Memory | O(depth) — the call stack | Usually O(1) |
| Stack overflow risk | Yes — when depth grows large | No |
| Speed | Slightly slower — call overhead | Slightly faster |
| Debugging | Harder — deep stack traces | Easier |
| When to choose | Trees/graphs, backtracking, divide and conquer | Linear traversal, or depth unknown or large |
Converting recursion to iteration — there are two typical scenarios.
- Linear (tail-shaped) recursion: walking a list, summing values, factorial. These convert directly into a
while/forloop, keeping the running result in an accumulator variable. Memory drops fromO(n)toO(1). - Branching recursion: tree traversal, DFS. Here you replace the call stack with an explicit stack (an array or
ArrayDeque): push a node, pop it, process it, push its children. The logic is the same, but since the memory is on the heap the depth limit effectively disappears.
Memoization is a separate improvement: if the same sub-problem is recomputed over and over, cache the results in a map. The classic example is fibonacci: the naive recursion is O(2^n), because computing fib(30) recomputes fib(10) thousands of times. With memoization each n is computed once — O(n) time, O(n) space. This is the doorway into dynamic programming.
Interview tip. When explaining a recursive solution, state three things in order: (1) what the base case is, (2) how the problem shrinks, (3) time and space complexity — always counting the call stack in space. Hitting that trio signals "this person understands recursion".
The most common question: "What's the complexity of naive recursive fibonacci and how would you improve it?" The expected answer: O(2^n) time / O(n) space (stack depth); with memoization O(n)/O(n); with a bottom-up iterative version O(n) time / O(1) space. Walking through those three tiers in order is a very strong answer.
The most common mistakes: forgetting or mis-specifying the base case (writing n == 1 and never handling n == 0); leaving the call stack out of the space complexity; and forcing recursion onto every problem — choosing recursion where a plain loop reads better is a negative signal.