05 Recursion
A progressive guide to designing, tracing, analyzing, and evaluating recursive algorithms, including call-stack behavior, common patterns, memoization, and the trade-offs between recursion and iteration.
Foundations of Recursive Thinking
solves a problem by applying the same function to a smaller or simpler instance of that problem. A recursive algorithm is appropriate when the problem has a natural self-similar structure, such as a tree, a sorted search range, a linked list, or a set of choices.
A correct recursive design has two essential parts:
The gives a direct answer and stops further calls.
The makes progress toward the and uses the smaller result to solve the current problem.
For factorial, the mathematical definition is:
The recursive definition is:
The call on is smaller than the current problem, so repeated calls eventually reach . The same design pattern appears in a sum function:
The key question is not simply whether a function calls itself. The key question is whether every possible execution path eventually reaches a valid stopping condition.
Tracing Calls with the
Each active function call occupies a frame in the . A frame stores information such as parameter values, local variables, the return location, and temporary computation state. When a function calls itself, a new frame is pushed. When the deeper call returns, its frame is removed and execution continues in the previous frame.
For a factorial call with input , calls are created in this order:
After the returns, results are combined in reverse order:
This last-in, first-out behavior explains why can use substantial memory even when each individual call uses little memory. For this factorial implementation, the maximum stack depth is , so the call-stack space is . If calls continue indefinitely or become too deep, the program may encounter a or a runtime -limit error.
Takeaway: Trace both the downward sequence of calls and the upward sequence of returns. The maximum number of simultaneously active calls determines stack depth.
Designing and Verifying Recursive Algorithms
A systematic design process makes recursive algorithms easier to verify:
Define precisely what the function returns for an input of size .
Identify the simplest valid input and use it to define the .
Reduce the input so that the recursive call receives a smaller problem.
Assume the smaller recursive call correctly solves its stated subproblem.
Use that result to construct the answer for the current input.
Verify that every path makes progress toward a .
Analyze both running time and space usage.
The commonly has three operations: reduce, call, and combine. In factorial, the reduction changes to , the call computes , and the combination multiplies that result by .
A must also have the correct value. Using would stop the but produce incorrect results, because the correct identity is . Similarly, a function such as bad(n) that calls itself with the unchanged argument has no meaningful progress, even if it contains a condition for .
Takeaway: Correctness requires both termination and a correct relationship between the current result and the smaller result.
Common Recursive Patterns
Different recursive structures lead to different performance patterns.
Decrease-and-conquer: One call handles a smaller problem, often of size . For a sum of the first integers, there are calls, giving time and stack space .
: A problem is divided into smaller parts. Binary search makes one call on approximately half the range, giving the recurrence and running time .
Recursive traversal: A tree function can process a node and then recursively process its children. With nodes, each node is processed once, so time is . Stack space is , where is the tree height.
First-and-rest processing: A sequence function can inspect one element and recursively process the remaining elements. A worst-case linear search has time and stack space .
: The algorithm explores choices, records complete solutions, and reverses choices before trying alternatives. Its time can be exponential when the number of possible choices grows rapidly.
For two half-sized recursive calls with linear combination work, the recurrence is:
This is the pattern associated with merge sort and gives running time .
Analyzing Time and Space
A makes the structure of recursive work explicit. The number of calls, the size of each subproblem, and the nonrecursive work at each call determine the final complexity.
For one call on a problem of size :
For one call on a problem of size approximately :
For two half-sized calls with linear work to combine results:
Branching call trees require attention to the branching factor, depth, work at each call, and repeated subproblems. Direct recursive Fibonacci illustrates the danger of repeated work:
The same smaller values are computed many times, producing commonly expressed exponential running time . stores each computed value, so values from through can be computed once. This reduces the time to and uses additional table space.
When analyzing space, distinguish call-stack space from . A memoized algorithm can have both a stack and a table of stored results.
Choosing Between and Iteration
and iteration can express the same computation with different resource usage. A factorial loop performs the multiplications from through without creating one call frame per input value. Both the straightforward recursive and iterative versions take time , but the recursive version uses call-stack space while the iterative version can use auxiliary space.
is often a good choice when:
The data is recursively structured, especially a tree.
The problem naturally divides into smaller subproblems.
Recursive code expresses the solution more clearly than an explicit simulation.
is required.
Iteration may be preferable when:
Input depth can be very large.
Constant auxiliary space is important.
The process is naturally sequential.
The language has a restrictive limit or does not optimize tail calls.
Before choosing , check for missing base cases, unchanged arguments, incorrect base-case values, repeated subproblems, and excessive depth. A correct algorithm can still fail on large inputs if its grows beyond the runtime limit. An iterative version, an explicit stack, or a more balanced strategy may address that limitation.
Final takeaway: Choose for structure and clarity when its depth and memory costs are acceptable; choose iteration when predictable space usage or very large depth matters.