CS205 Data Structures
Use arrow keys or buttons to navigate · Press S to reveal steps
A recursive function is a function that calls itself to solve smaller instances of the same problem.
Each doll contains a smaller doll inside it. You keep opening dolls until you reach the smallest one (the base case) -- then you stop.
Recursion works because each call solves a smaller version of the same problem, until the problem is so small we know the answer directly.
Every correct recursive function needs exactly two things:
The condition under which the function does NOT call itself. This is the "smallest doll" -- the answer we know directly.
The function calls itself with a smaller or simpler input, making progress toward the base case.
If you forget the base case, or if the recursive case doesn't make progress, you get infinite recursion and a StackOverflowError.
n! = n * (n-1) * (n-2) * ... * 1 and 0! = 1
factorial(4) can't finish until factorial(3) finishes, which can't finish until factorial(2) finishes, and so on down to factorial(0).
The call stack is how the computer keeps track of which function called which, what each function's local variables are, and where to return to.
The stack has a finite size (typically a few MB). Too many recursive calls without returning will exhaust it, causing a StackOverflowError.
Let's trace factorial(4) showing the stack at every step.
Press S to reveal each step.
fib(0)=0, fib(1)=1, fib(n) = fib(n-1) + fib(n-2) for n ≥ 2
| n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| fib(n) | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
Unlike factorial (one recursive call), Fibonacci makes two recursive calls per invocation. This creates a binary tree of calls.
The recursion tree has O(2n) nodes because we recompute the same values over and over.
fib(50) would take minutes. fib(100) would take longer than the age of the universe.
Store results of subproblems so each is computed only once. This drops the time from O(2n) to O(n). This technique is the foundation of dynamic programming.
| Approach | Time | Space |
|---|---|---|
| Naive recursion | O(2n) | O(n) stack |
| Memoized | O(n) | O(n) |
| Iterative | O(n) | O(1) |
Compute the sum of an array recursively: peel off one element at a time.
Imagine a stack of bills. To count the total: take the top bill, add its value to the total of the remaining stack. If the stack is empty, the total is $0.
Search a sorted array by repeatedly halving the search space. Classic divide and conquer.
Each recursive call eliminates half the remaining elements. This gives us O(log n) time -- far better than linear search's O(n).
Compute xn recursively. Two approaches with very different performance.
Each call reduces n by 1, so we make n calls.
By squaring the half-result, we halve the exponent each time instead of subtracting 1. This is exponentiation by squaring.
Recursive functions are classified by how many recursive calls they make.
Linear recursion creates a chain. Stack depth equals the number of calls. Easy to convert to iteration.
Binary recursion can be exponential in total calls even though the stack depth is only O(n). The tree can have far more nodes than its height.
A recursive call is a tail call if it is the very last operation the function performs -- nothing happens after the call returns.
Tail recursion carries the result in an accumulator parameter. The compiler can optimize this into a simple loop -- same speed, O(1) stack space.
Java and Python do not optimize tail calls. Functional languages (Haskell, Scheme, Scala) typically do.
Any recursive algorithm can be converted to iteration (and vice versa). When should you use which?
| Aspect | Recursion | Iteration |
|---|---|---|
| Clarity | Often cleaner for trees, divide & conquer | Cleaner for simple loops |
| Stack | O(n) frames | O(1) |
| Speed | Function call overhead | Slightly faster |
| Risk | Stack overflow | Infinite loop |
Recursion is like giving instructions to a chain of helpers -- each passes a smaller task down. Iteration is like doing it yourself in a loop. Both finish the job; pick the one that's clearest.
Most recursive algorithms fall into one of these three families.
Split the problem into independent subproblems, solve each recursively, then combine results.
Examples: merge sort, quicksort, binary search, fast power
Reduce the problem by a constant amount (usually 1) each step. Linear chain of calls.
Examples: factorial, sum of array, insertion sort, selection sort
Try a choice, recurse. If it fails, undo the choice and try the next option.
Examples: N-queens, maze solving, sudoku, generating permutations
Move n disks from peg A to peg C using peg B as auxiliary. Rules: move one disk at a time; never place a larger disk on a smaller one.
Press S to step through the solution for 3 disks.
To move n disks: (1) move the top n-1 disks out of the way, (2) move the big disk, (3) move the n-1 disks on top. It takes 2n - 1 moves.
For 3 disks: 23 - 1 = 7 moves
Debugging recursion can be tricky. Watch out for these pitfalls.
Fix: Always define when to stop.
Fix: Each call must move closer to the base case.
Fix: Consider all possible inputs, including edge cases.
Fix: Use memoization or convert to iteration.
Add print statements at the start of the function showing the parameters, and at the end showing the return value. This makes the call tree visible.
| Concept | What It Means |
|---|---|
| Base Case | When to stop recursing |
| Recursive Case | Self-call with smaller input |
| Call Stack | Frames tracking each call |
| Stack Overflow | Too-deep recursion |
| Tail Recursion | Recursive call is last op |
| Memoization | Cache repeated subproblems |
| Divide & Conquer | Split, solve halves, combine |
| Backtracking | Try, recurse, undo if fail |
Recursion is the art of solving a problem by saying: "If I could magically solve a slightly smaller version of this problem, how would I use that to solve the original?" Then you just need to handle the tiniest case directly.
CS205 Data Structures · Recursion