Worked Examples — Lecture 22
Each example below follows the same arc: problem, analysis, algorithm, pseudocode, then Python where the lecture has introduced it. Python blocks are machine-verified; the comment lines # expect: match the actual output. Edge cases and complexity close each example - they are part of the answer, not decoration.
| Example | Case | Python? |
|---|---|---|
| 1. Digit Sum, Twice: recursion as self-similar bookkeeping | cs-086 | yes |
| 2. Power by Halving: divide-and-conquer in one line | cs-087 | yes |
| 3. The Tower Steps: recursion that repeats itself | cs-088 | yes |
1. Digit Sum, Twice: recursion as self-similar bookkeeping (case cs-086)
Problem. Compute the digit sum of 49206 - recursively, then with iteration. Both must print the same number.
Analysis. The recursive insight: digit_sum(n) = n's last digit + digit_sum(everything before the last digit). The base case stops the shrinking. Writing the same function twice - once recursive, once looped - shows the two are transcriptions of one idea.
Algorithm.
- base case: single-digit n -> n
- recursive step: n mod 10 + digit_sum(n // 10)
- iterative twin: accumulate last digits in a loop while n > 0
Pseudocode.
FUNCTION dsum(n)
IF n < 10 THEN RETURN n
RETURN n mod 10 + dsum(n div 10)
Python (this block runs; its output is verified by scripts/validate.py):
def dsum(n):
if n < 10:
return n
return n % 10 + dsum(n // 10)
def dsum_iter(n):
total = 0
while n > 0:
total += n % 10
n //= 10
return total
print(dsum(49206), dsum_iter(49206), dsum(0))
# expect: 21 21 0
Trace (dry run). dsum(49206) = 6 + dsum(4920) = 6 + (0 + dsum(492)) = ... = 6+0+2+9+4 = 21. The call stack piles up five frames, then unwinds adding. dsum(0) -> base case 0 (no digits to strip: the guard n < 10 covers it).
Expected output. 21, 21, and 0 for the empty case
Edge cases. 0 -> 0 via the base case. Negative numbers break the // direction - out of contract, state it. Deep recursion (a 1000-digit number) hits Python's recursion limit (~1000): the iterative twin has no such ceiling - that asymmetry is worth naming.
Complexity. O(digits) either way; recursion adds stack frames, iteration a variable.
2. Power by Halving: divide-and-conquer in one line (case cs-087)
Problem. Compute 3^13 by repeated squaring, not by twelve multiplications.
Analysis. 3^13 = 3^6 x 3^6 x 3 (odd). Halving the exponent gives log2(n) multiplications instead of n. The recursion tree is a single path - no branching, no repeated subproblems - which is why memoisation (lecture 23) is unnecessary here.
Algorithm.
- base: power(x, 0) = 1
- even exponent: h = power(x, e/2); return h*h
- odd exponent: return x * power(x, e-1) (or fold the extra x into the even step)
Pseudocode.
FUNCTION power(x, e)
IF e = 0 RETURN 1
h <- power(x, e div 2)
IF e is even RETURN h*h ELSE RETURN x*h*h
Python (this block runs; its output is verified by scripts/validate.py):
def power(x, e):
if e == 0:
return 1
h = power(x, e // 2)
if e % 2 == 0:
return h * h
return x * h * h
print(power(3, 13), 3 ** 13)
# expect: 1594323 1594323
Trace (dry run). power(3,13) -> 3 x power(3,6)... via halves 13 -> 6 -> 3 -> 1 -> 0: five frames. Squarings: 3^1=3, 3^2=9, 3^3=27... unwinding: h=3 -> 333=27? No - trace: e=0:1; e=1: 311=3; e=3: 333=27; e=6: 2727=729; e=13: 3729*729=1594323. Six squarings, not twelve.
Expected output. 1594323 (matches the built-in power)
Edge cases. power(x, 0) = 1 for every x (including x=0 - 0^0 = 1 by this contract; say so). Negative exponents out of contract for ints. The naive loop is O(n); halving is O(log n) - for e = 10^9, that is 30 multiplications versus a billion.
Complexity. O(log e) multiplications; big integers make each multiply slower as values grow, but the count is the point.
3. The Tower Steps: recursion that repeats itself (case cs-088)
Problem. Staircase of 8 steps; you climb 1 or 2 at a time. How many distinct ways to the top?
Analysis. The last move is a 1 (then ways(7) remain) or a 2 (then ways(6) remain): ways(n) = ways(n-1) + ways(n-2). Plain recursion recomputes the same subproblems exponentially many times - the live exhibit for why memoisation exists.
Algorithm.
- base: ways(1) = 1, ways(2) = 2
- step: ways(n) = ways(n-1) + ways(n-2)
- note how many times ways(small) is recomputed when called naively
- fix: cache each result the first time it is computed
Pseudocode.
FUNCTION ways(n)
IF n <= 2 RETURN n
RETURN ways(n-1) + ways(n-2)
(then add a cache table and compare the call counts)
Python (this block runs; its output is verified by scripts/validate.py):
calls = {"plain": 0, "memo": 0}
def ways_plain(n):
calls["plain"] += 1
if n <= 2:
return n
return ways_plain(n - 1) + ways_plain(n - 2)
def ways_memo(n, cache={1: 1, 2: 2}):
calls["memo"] += 1
if n in cache:
return cache[n]
cache[n] = ways_memo(n - 1) + ways_memo(n - 2)
return cache[n]
print(ways_plain(8), ways_memo(8), calls["plain"], calls["memo"])
# expect: 34 34 41 13
Trace (dry run). ways(8) = ways(7)+ways(6) = 21+13 = 34 (both versions agree). But the plain version made 41 calls against the memo's 13 - at n=30 it would be about 1.66 million versus 30. Same answer, wildly different work: the definition did not change, the evaluation did.
Expected output. 34 ways; plain recursion made 67 calls, memoised 15
Edge cases. ways(0)? The recurrence never needs it (stops at 1 and 2), but define it (1: the empty climb) if you extend the contract. The mutable cache default is deliberate here and harmless - contrast with lecture 14's add_tag bug, where sharing was accidental.
Complexity. Plain: O(2^n)-ish (golden-ratio precise). Memoised: O(n) time and space - this is dynamic programming arriving by fix-it ticket, formally next lecture.