Exercises — Classic recursion — factorial, Fibonacci, binary search
Before we start, one reminder of the two pieces every recursion has (from the parent note):
Level 1 — Recognition
"Can you spot the pieces?"
L1.1 — Identify the base case
Recall Solution
Base case: if n == 0: … return.
Why: it is the only branch that returns without calling countdown again. It is reached when the input has shrunk all the way down to . Without it, countdown(n-1) would keep going to forever → RecursionError.
L1.2 — Read the recurrence off the code
Recall Solution
Why ? The total up to is " itself" glued onto "the total up to " — that is the self-similar structure. Why ? Summing no numbers gives (the empty sum), and it stops the chain.
L1.3 — Count the base cases
Recall Solution
Two base cases: and . Why two? The recurrence reaches back two steps. If you only anchored , then computing would ask for — below the floor — and never stop. You need as many fixed starting values as the number of steps the recurrence looks back.
Level 2 — Application
"Can you run the templates?"
L2.1 — Trace factorial
Recall Solution
factorial(4) = 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * (1 * factorial(0))))
= 4 * (3 * (2 * (1 * 1))) # base case 0! = 1
= 24
Why this order? Every multiplication waits on the call stack until the base case factorial(0)=1 returns, then the products resolve bottom-up: . Answer: .
L2.2 — Hand-run binary search
Look at the picture — each row is one call, and the shaded band is the part of the list still "alive".

Recall Solution
Indices: 2→0, 4→1, 6→2, 8→3, 10→4, 11→5, 14→6. Start lo=0, hi=6.
| call | lo | hi | mid | arr[mid] | decision |
|---|---|---|---|---|---|
| 1 | 0 | 6 | 3 | 8 | → go right, lo=4 |
| 2 | 4 | 6 | 5 | 11 | found → return 5 |
mid = (0+6)//2 = 3; since the whole left half (indices 0–3) is thrown away. Next mid = (4+6)//2 = 5, and arr[5]==11. Answer: returns index .
L2.3 — Write power(base, n)
Recall Solution
Recurrence: (one factor of peeled off), base: (empty product).
def power(b, n):
if n == 0: # base case: b^0 = 1
return 1
return b * power(b, n - 1) # recursive caseTrace: . Answer: .
Level 3 — Analysis
"Can you reason about cost and behaviour?"
L3.1 — How deep does the stack go?
Recall Solution
Frames exist for factorial(100), factorial(99), …, factorial(0) — that's every integer from down to inclusive: frames.
General: factorial(n) reaches depth . Answer for 100: frames.
This is why very large can trigger RecursionError — Python's default recursion limit is around frames.
L3.2 — Count the calls in naive Fibonacci
Study the call tree — every circle is one call, and you simply count circles.

Recall Solution
Recurrence for the counter: one call for fib(n) itself, plus all calls its two children make:
Building up:
Answer: calls. (Notice is far more than the distinct values — the extra are redundant recomputations. See Big-O Notation.)
L3.3 — How many comparisons does binary search make?
Recall Solution
Each comparison halves the remaining range, so the worst case is the number of halvings needed to shrink down to : Check: , so you can halve at most times. Answer: comparisons. Compare to linear search's worst case of — that gap is the payoff of .
Level 4 — Synthesis
"Can you build new recursions?"
L4.1 — Recursive sum of a list
Recall Solution
Idea: the sum of a list is "its first element" plus "the sum of the rest".
def sum_list(a):
if len(a) == 0: # base case: empty list sums to 0
return 0
return a[0] + sum_list(a[1:]) # first + sum of the restRecurrence: , base . Trace: . Answer: .
L4.2 — Reverse a string recursively
Recall Solution
Idea: move the first character to the end, after reversing the rest.
def reverse(s):
if len(s) <= 1: # base case: empty or single char is its own reverse
return s
return reverse(s[1:]) + s[0] # reverse the tail, then append the headTrace:
reverse("cat") = reverse("at") + "c"
= (reverse("t") + "a") + "c"
= ("t" + "a") + "c" # base: "t" reverses to "t"
= "tac"
Answer: "tac".
L4.3 — Recursive gcd (Euclid's algorithm)
Recall Solution
def gcd(a, b):
if b == 0: # base case: gcd(a,0)=a
return a
return gcd(b, a % b) # smaller problem: remainder shrinks fastTrace:
gcd(48,18) = gcd(18, 48%18) = gcd(18, 12)
= gcd(12, 18%12) = gcd(12, 6)
= gcd(6, 12%6) = gcd(6, 0)
= 6 # base case
Why does it stop? The remainder is always strictly smaller than , so b decreases every call and must hit . Answer: .
Level 5 — Mastery
"Subtle traps and full re-derivations."
L5.1 — The mutable default argument bug
Recall Solution
In Python, a default argument is created once, when the function is defined — not on each call. So the same memo dict is shared across every call that doesn't pass its own.
- Danger: the cache from
fib(50)persists intofib(30). Here that's harmless (the cached Fibonacci values are still correct and it even speeds things up), but the same pattern with a list default (e.g.def add(x, box=[])) causes results to leak between calls — a classic bug. - Safe fix: use
memo=Noneand setif memo is None: memo = {}inside, giving each top-level call a fresh cache while still sharing it down the recursion. Key insight: correctness of memoizedfibhere survives the shared dict only because Fibonacci values never change; don't rely on that luck in general.
L5.2 — Fast exponentiation (divide & conquer)
Recall Solution
Idea (a Divide and Conquer move): if is even, — compute the half-power once, then square it. If is odd, .
def fast_power(b, n):
if n == 0:
return 1
half = fast_power(b, n // 2)
if n % 2 == 0:
return half * half
return b * half * halfTrace :
3^10 = (3^5)^2
3^5 = 3 * (3^2)^2
3^2 = (3^1)^2
3^1 = 3 * (3^0)^2 = 3*1*1 = 3
So , , . Recurrence: — same shape as binary search. Answer: .
L5.3 — Why sortedness is non-negotiable
Recall Solution
Start lo=0, hi=4. mid = (0+4)//2 = 2, arr[2] = 5. Since , the code assumes everything left of index 2 is smaller and throws it away, searching right with lo=3.
call 1: lo=0 hi=4 mid=2 arr[mid]=5 -> 5<7 -> go right lo=3
call 2: lo=3 hi=4 mid=3 arr[mid]=3 -> 3<7 -> go right lo=4
call 3: lo=4 hi=4 mid=4 arr[mid]=7 -> found, return 4
Here it accidentally found it — but that's luck. If target = 1 (at index 1), the first step would send it left to indices 0–1, where arr is [9,1], and depending on comparisons it may return the wrong index or -1.
Why it breaks: the halving logic depends on "left of mid ≤ mid ≤ right of mid", which is exactly the sorted property. Remove that guarantee and eliminating a half is unjustified. Fix: sort first (, see Sorting Algorithms) or use linear search.
Recall One-line answer key (self-check)
L2.1 24 · L2.2 5 · L2.3 32 · L3.1 101 · L3.2 15 · L3.3 10 · L4.1 14 · L4.2 "tac" · L4.3 6 · L5.2 59049
Connections
- Recursion vs Iteration — every solution here could be rewritten as a loop
- Big-O Notation — L3 is entirely about deriving these costs
- Memoization and Dynamic Programming — L5.1's caching trap
- Divide and Conquer — L5.2 fast exponentiation and binary search share the shape
- The Call Stack and Stack Frames — L3.1 stack depth
- Sorting Algorithms — L5.3's fix