Chapter 11: Recursion
1. What is Recursion?
Imagine standing between two mirrors: you see a reflection inside a reflection, getting smaller each time. In programming, recursion is similar— a function keeps calling itself, each time working on a tinier version of the original problem, until it reaches a case so small it can finish immediately.
2. Two Rules Every Recursive Function Follows
| Rule | What it means | In code |
|---|---|---|
| Base case | The “easy” situation where you already know the answer. Stops the chain of calls. | if n <= 0: return 0 |
| Recursive case | Breaks the bigger task into a smaller one and calls itself again. Must move closer to the base case. | return n + recursive_function(n-1) |
If a base case is missing or never reached, the calls never stop and you get a stack overflow error.
3. Three Flavors of Recursion
| Type | Idea | Tiny example |
|---|---|---|
| Direct | A function calls itself. | factorial(n) → factorial(n-1) |
| Indirect | Function A calls B, which calls A, … | is_even() ↔ is_odd() |
| Tail | The very last action is the recursive call, so some languages can optimize away extra stack frames. | factorial_tail(n, acc) |
Tip: Python does not automatically optimize tail recursion, but the style can still make intent clearer.
4. Worked-Through Examples
A. Sum from 1 to n
def sum_to_n(n):
if n == 0: # base
return 0
return n + sum_to_n(n-1) # recursiveCall flow for n = 3
sum_to_n(3)
└─ 3 + sum_to_n(2)
└─ 2 + sum_to_n(1)
└─ 1 + sum_to_n(0)
└─ 0 ← base caseThen results bubble back: 0 → 1 → 3 → 6.
B. Factorial (direct recursion)
5! = 5 × 4 × 3 × 2 × 1
def factorial(n):
if n <= 1:
return 1
return n * factorial(n-1)C. Even / Odd (indirect recursion)
def is_even(n):
if n == 0: return True
return is_odd(n-1)
def is_odd(n):
if n == 0: return False
return is_even(n-1)Each call shrinks n by 1, so one of them will eventually reach 0.
D. Tail-recursive Factorial
def fact_tail(n, acc=1):
if n <= 1:
return acc
return fact_tail(n-1, n*acc)All the “work” is stored in acc, so the last thing done is the call itself.
E. Fibonacci
Plain recursive Fibonacci mirrors the definition
F(n) = F(n-1) + F(n-2) with F(0)=0, F(1)=1.
⚠️ Note: This naive version repeats work and grows exponentially slow. For bigger n use memoization (caching) or an iterative loop.
F. Binary Search
Recursion fits naturally because each step cuts the list in half. Base cases are “found it” or “ran out of space”.
G. Greatest Common Divisor (GCD)
The Euclidean algorithm: gcd(a, b) = gcd(b, a % b) until b == 0.
Each step shrinks one argument, so it converges quickly.
H. List & String Tricks
- List sum / list reverse
- String reverse / palindrome check
These all follow the same pattern: peel off one element/character, solve the rest recursively, then combine.
5. When to Choose Recursion
| Good fit | Not a good fit |
|---|---|
| Problems naturally defined in terms of smaller copies of themselves (trees, divide-and-conquer, backtracking). | Situations where the depth might be thousands—Python’s default recursion limit is about 1000, after which you hit RecursionError. |
6. Best Practices Checklist ✔️
- Clear base case—guarantee a stop.
- Progress toward base every call.
- Understand cost: each call adds a stack frame (memory).
- Consider alternatives: tail recursion, memoization, or iterative loops.
- Test with small inputs first—they’re easier to debug and trace.
7. Big-O at a Glance
| Problem | Time complexity | Notes |
|---|---|---|
| naive Fibonacci | O(φⁿ) exponential | memoize to O(n) |
| Factorial, sum, GCD | O(n) | linear in n |
| Binary search | O(log n) | halves list each step |
| List reverse (naive) | O(n²) | slicing makes copies; use indices or iterate for O(n) |
8. Key Takeaway
Recursion is just “a problem solving itself with less baggage each time.” If you can (1) state a simple base case and (2) shrink the problem on every step, you have a safe, elegant solution.