Recursion
High SchoolRecursion is when a function solves a problem by calling itself on a smaller instance of the same problem. Every correct recursion has two ingredients: one or more base cases that are solved directly without further recursion, and a recursive case that reduces the problem toward a base case. Omit the base case and you get infinite recursion; fail to make progress toward it and the same.
Recursion is powered by the call stack: each call gets its own stack frame holding its local variables and its return address. Calls stack up as the recursion descends and unwind — in strict LIFO order — as each returns. This is the same stack structure from Chapter 5, now doing the language's own bookkeeping.
Many powerful techniques are inherently recursive: divide and conquer (merge sort), tree and graph traversal, and problems with self-similar structure. A recurrence like T(n) = 2·T(n/2) + O(n) captures such costs and, by the Master Theorem, resolves to O(n log n).
The trick to trusting recursion is the leap of faith: assume the function already works correctly for smaller inputs, then only make sure you (a) handle the base case and (b) combine the smaller solution correctly. If both hold, induction guarantees the whole thing is right. You do not — and should not — trace every level in your head.
Recursion and iteration are equally powerful; anything one can do, so can the other. The choice is about clarity. Tree-shaped problems read beautifully as recursion and painfully as loops-with-explicit-stacks. But recursion has a cost: each pending call consumes a stack frame, so very deep recursion can overflow the stack — where a loop would use constant space.
Insight. A recursive function is a proof by induction that happens to run. The base case is the induction basis; the recursive case is the inductive step.
Three classics: factorial (linear recursion), Fibonacci (tree recursion, with a memoised fix), and the Tower of Hanoi (exponential but elegant).
def factorial(n):
if n == 0: # base case
return 1
return n * factorial(n - 1) # recursive case: shrink toward 0
def fib(n): # naive: O(2^n) -- recomputes the same values
if n < 2:
return n # base cases: fib(0)=0, fib(1)=1
return fib(n - 1) + fib(n - 2)
def fib_fast(n, memo=None): # memoised: O(n)
if memo is None:
memo = {}
if n < 2:
return n
if n not in memo:
memo[n] = fib_fast(n - 1, memo) + fib_fast(n - 2, memo) # cache result
return memo[n]
def hanoi(n, source, target, spare):
"""Move n disks from source to target using spare, printing each move."""
if n == 0:
return # base case: nothing to move
hanoi(n - 1, source, spare, target) # move top n-1 out of the way
print("move disk " + str(n) + ": " + source + " -> " + target)
hanoi(n - 1, spare, target, source) # move them onto the target
print(factorial(5)) # 120
print(fib_fast(30)) # 832040 (instant; naive fib(30) does ~2.7M calls)
hanoi(3, "A", "C", "B") # 7 moves = 2^3 - 1
Naive fib is a cautionary tale: it recomputes fib(2) exponentially many times. Memoisation — caching each answer the first time — collapses it from O(2ⁿ) to O(n). Hanoi shows the opposite: 2ⁿ−1 moves is optimal, an inherently exponential task expressed in four tidy lines.
Q1 What exactly causes a "stack overflow"?
Each unreturned function call keeps a frame on the call stack, which has finite size. If recursion goes too deep — say a missing base case, or recursing 100,000 levels — the frames exhaust the stack's memory and the program crashes with a stack-overflow error. Iterative solutions or tail-call optimisation (where supported) avoid this.
Q2 Why is naive Fibonacci so slow?
Because it recomputes overlapping subproblems. fib(5) calls fib(4) and fib(3); fib(4) also calls fib(3); and so on, forming a branching tree with ~2ⁿ nodes. Memoisation (or bottom-up dynamic programming) stores each result once, reducing the count to n distinct subproblems.
Q3 Is recursion ever strictly necessary?
No — any recursion can be rewritten as a loop plus an explicit stack, and vice versa. The choice is expressiveness, not power. For self-similar, tree-shaped data (parsers, file systems, divide-and-conquer), recursion is dramatically clearer. For simple linear repetition, iteration is usually simpler and avoids stack limits.
Q4 How do I know my recursion will terminate?
Identify a measure — a non-negative quantity that strictly decreases on every recursive call and triggers a base case at its minimum. For factorial(n) the measure is n, dropping by 1 each time until it hits the base case at 0. If you cannot name such a decreasing measure, you have no termination guarantee.
How the ideas connect
Every key idea in this chapter, branching from the core concept — use it to see the whole picture at a glance.
The process, step by step
Worked problems, step by step
Follow each solution line by line, then try to reproduce it on paper before moving on.
Example 1Compute factorial(4) where n! = n * (n-1)! and 0! = 1.
- 4! = 4 * 3!
- 3! = 3 * 2!, 2! = 2 * 1!, 1! = 1 * 0!, 0! = 1.
- Multiply back up: 1,1,2,6,24.
Example 2Give the 5th Fibonacci number where F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2).
- 0, 1, 1, 2, 3, 5...
- F(5) is the term after 3.
Now you try
Work each one out first, then tap to reveal the worked answer.