fib(0)=0, fib(1)=1, fib(n) = fib(n-1) + fib(n-2) for n ≥ 2
Code
functionfib(n):if n == 0: return0// baseif n == 1: return1// basereturnfib(n-1) + fib(n-2)
Sequence
n
0
1
2
3
4
5
6
7
fib(n)
0
1
1
2
3
5
8
13
Key Idea
Unlike factorial (one recursive call), Fibonacci makes two recursive calls per invocation. This creates a binary tree of calls.
Recursion Tree for fib(n)
Draws the full recursion tree — redundant calls in red
Why Naive Fibonacci Is Slow
The recursion tree has O(2n) nodes because we recompute the same values over and over.
Repeated Subproblems
In fib(5), look how many times
each value is computed:
fib(5): 1 time
fib(4): 1 time
fib(3): 2 times <-- waste!
fib(2): 3 times <-- waste!
fib(1): 5 times <-- waste!
fib(0): 3 times <-- waste!
For fib(40):
~331 million calls!
But only 41 unique subproblems.
Exponential Growth
fib(50) would take minutes. fib(100) would take longer than the age of the universe.
Naive vs Memoized — Live Comparison
Naive O(2n)
0
calls made
Memoized O(n)
0
calls made
Click "Run Both" to compare call counts
// The Fix: Memoizationfunctionfib_memo(n, memo={}):if n in memo: return memo[n]if n == 0: return0if n == 1: return1 memo[n] = fib_memo(n-1) + fib_memo(n-2)return memo[n]
Approach
Time
Space
Naive recursion
O(2n)
O(n) stack
Memoized
O(n)
O(n)
Iterative
O(n)
O(1)
Example: Sum of an Array
Compute the sum of an array recursively: peel off one element at a time.
Code
functionsum(arr, n):// n = number of elementsif n == 0: // base casereturn0return arr[n-1] + sum(arr, n-1)
Analogy
Imagine a stack of bills. Take the top bill, add its value to the total of the remaining stack. If empty, total is $0.
Click Step to trace sum([3, 7, 2, 5], 4)
Array & Stack
Example: Binary Search (Recursive)
Search a sorted array by repeatedly halving the search space. Classic divide and conquer.
Each call eliminates half the remaining elements → O(log n) time.
Interactive Search
Search arr = [1, 3, 5, 7, 9, 11, 13]
Example: Power Function
Compute xn recursively. Two approaches with very different performance.
Naive: O(n) Slow
functionpower(x, n):if n == 0: return1return x * power(x, n-1)
Fast: O(log n) Fast
functionfastPower(x, n):if n == 0: return1 half = fastPower(x, n / 2)if n is even:return half * halfelse:return x * half * half
Key Idea
Squaring the half-result halves the exponent each time. This is exponentiation by squaring.
Live Comparison
Naive O(n)
Fast O(log n)
Click Compare to see step-by-step traces
Linear vs Binary/Multiple Recursion
Recursive functions are classified by how many recursive calls they make.
Linear Recursion (1 call) Chain
Examples: factorial, sum, binary search
Depth & total calls: O(n) or O(log n)
Key Idea
Linear recursion creates a chain. Easy to convert to iteration.
Binary/Multiple Recursion (2+ calls) Tree
Examples: fibonacci, merge sort, Towers of Hanoi
Depth: O(n) or O(log n), but total calls up to O(2n)!
Warning
Binary recursion can be exponential in total calls even though stack depth is only O(n).
Tail Recursion
A recursive call is a tail call if it is the very last operation — nothing happens after the call returns.
NOT Tail Recursive
functionfactorial(n):if n == 0: return1return n * factorial(n-1) // multiply AFTER call
Tail Recursive Version
functionfactorial(n, acc=1):if n == 0: return accreturnfactorial(n-1, n*acc) // call IS the return
Note
Java and Python do not optimize tail calls. Functional languages (Haskell, Scheme, Scala) typically do.
Stack Comparison (n = 4)
Non-Tail (stack grows)
Tail (1 frame reused)
Click Animate to see the difference
Key Idea
Tail recursion carries the result in an accumulator. The compiler can reuse a single frame — same speed, O(1) stack.
Recursion vs Iteration
Any recursive algorithm can be converted to iteration (and vice versa). When should you use which?
Side by Side: Factorial
// RECURSIVEfunctionfactorial(n):if n == 0:return1return n *factorial(n-1)
// ITERATIVEfunctionfactorial(n): result = 1for i in1..n: result *= ireturn result
Aspect
Recursion
Iteration
Clarity
Cleaner for trees, D&C
Cleaner for simple loops
Stack
O(n) frames
O(1)
Speed
Call overhead
Slightly faster
Risk
Stack overflow
Infinite loop
When to Use Recursion
Problem has a recursive structure (trees, graphs, nested data)
Divide and conquer algorithms (merge sort, quicksort)
Backtracking problems (N-queens, mazes)
The recursive solution is much simpler than iterative
When to Use Iteration
Simple linear computations (sum, factorial)
Performance-critical code
Very deep recursion possible (n > 10000)
Language lacks tail-call optimization
Analogy
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.
Common Recursion Patterns
Most recursive algorithms fall into one of these three families. Click a pattern to see it animated.
Divide & Conquer
Split into independent subproblems, solve each, then combine.
Examples: merge sort, quicksort, binary search, fast power
Decrease & Conquer
Reduce by a constant amount (usually 1) each step. Linear chain.
Examples: factorial, sum of array, insertion sort
Backtracking
Try a choice, recurse. If fail, undo and try next option.
Examples: N-queens, maze, sudoku, permutations
Towers of Hanoi
Move n disks from peg A to peg C. Rules: one disk at a time; never place larger on smaller.
Recursive Solution
functionhanoi(n, from, to, aux):if n == 1: move disk 1 from 'from' to 'to'returnhanoi(n-1, from, aux, to) // top n-1 to aux move disk n from 'from' to 'to'hanoi(n-1, aux, to, from) // n-1 from aux to to
Key Idea
Move top n-1 out of the way, move the big disk, put n-1 back on top. Takes 2n - 1 moves.
Interactive Playground
Moves: 0 | Minimum: 7
Common Mistakes
Debugging recursion can be tricky. Watch out for these pitfalls.
1. Missing Base Case
// BROKEN — no base case!functionfactorial(n):return n * factorial(n-1)// → factorial(0) → factorial(-1) → ... OVERFLOW
Fix: Always define when to stop.
2. Not Making Progress
// BROKEN — n never changes!functioncountdown(n):if n == 0: return print(n)countdown(n) // should be n-1
Fix: Each call must move closer to the base case.
3. Wrong Base Case
// BROKEN — misses n=0!functionfactorial(n):if n == 1: return1return n * factorial(n-1)// factorial(0) → 0 * factorial(-1) → ...
Fix: Consider all inputs including edge cases.
4. Redundant Computation
// WORKS but O(2^n) slow!functionfib(n):if n <= 1: return nreturnfib(n-1) + fib(n-2)// fib(5) computes fib(2) three times!
Fix: Use memoization or convert to iteration.
Debugging Tip
Add print statements at the start of the function showing parameters, and at the end showing the return value. This makes the call tree visible.
Summary & Cheat Sheet
The Recursion Recipe
Identify the base case(s) — simplest inputs with known answers
Identify the recursive case — how to break into smaller pieces
Ensure each call makes progress toward a base case
Trust the recursion — assume the recursive call works for smaller input
Combine the result of the recursive call with the current step
Pattern Time Example
─────────────────────────────────────────
Decrease by 1 O(n) factorial
Decrease by half O(log n) bin search
Two calls (dep.) O(2^n)* fibonacci*
Two calls (indep.) O(n log n) merge sort
Backtracking varies N-queens
* without memoization
Key Concepts
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
Final Analogy
Recursion is the art of solving a problem by saying: "If I could magically solve a slightly smaller version, how would I use that to solve the original?" Then handle the tiniest case directly.
CS205 Data Structures · Recursion
Challenge: Predict the Stack
At step 3 of factorial(5), which shows the correct call stack?