Day 27 ยท Russian dolls

Recursion & Divide/Conquer

You will be able to
  • Write recursive functions with an explicit base case and a shrinking recursive case
  • Trace a recursive call on the call stack and predict its maximum depth
  • Draw the call tree of naive fib and explain why memoization collapses it to O(n)
  • Describe the divide-and-conquer shape (split, solve halves, combine) using merge sort
  • Convert a recursion to iteration with an explicit stack, and say when you must
Today's ~120 minutes
Spaced-rep warm-up: Days 23โ€“26 cards + due deck10 min
ELI5 + tech read, watch the call-stack visualizer20 min
Guided: stack breathing + nested data + merge sort40 min
Practice: fast pow + flatten20 min
Project: pattern log recursion section20 min
Quiz + flashcards10 min

Builds on: Day 3 โ€” Functions & scope ยท Day 12 โ€” Decorators & lru_cache ยท Day 25 โ€” Stacks โ€” the call stack's structure

The analogy

A set of Russian dolls, and your job is to count them. The honest method: open the outermost doll, and inside you findโ€ฆ a slightly smaller version of THE EXACT SAME PROBLEM. So you apply the same move again. Eventually you reach the tiny solid doll that doesn't open โ€” you don't recurse on it, you just say "one." That solid doll is the base case, and without it you'd be trying to open dolls forever. Then the answers reassemble on the way OUT: the innermost answer is 1, the next doll says "1 + what was inside me = 2," and so on outward until the outermost doll announces the total.

That's recursion: a function that calls itself on a *smaller* version of its problem, plus a smallest case solved directly. The half-opened dolls sitting on the table while you work inward? That's the call stack โ€” Day 25's tray pile, holding every paused caller until its inner answer comes back. Stack too many dolls (no base case, or input too deep) and the table overflows โ€” literally a *stack overflow*. Divide and conquer is the power move: instead of opening one doll at a time, split the problem in HALF, solve both halves the same way, and combine โ€” halving is why these algorithms end up O(n log n) instead of O(nยฒ).

Why this matters on the job

Recursion is the native language of nested data, and your career is full of nested data: directory trees, JSON documents from every API (Day 41), abstract syntax trees, org charts โ€” and next week's binary trees (Day 29) and graphs (Day 31) are traversed almost entirely by recursion. Merge sort's divide-and-conquer shape explains WHY sorting is O(n log n) (Day 32), and memoization is dynamic programming's front door (Day 34). Interviewers use recursion as a lens: a candidate who states the base case, the shrinking step, and the stack depth is demonstrating structured thinking, not syntax.

Watch it happen

Russian dolls โ€” the call stack runs fib(4)

step 1 / 6
bottom
fib(4) โ† top

Call fib(4). A FRAME is pushed holding its local state. fib(4) can't answer yet โ€” it needs fib(3) and fib(2) first.

Guided practice

guided 1

Watch the stack breathe

20 min
  1. In recursion_lab.py, write countdown(n) (print n, recurse on nโˆ’1, base case n == 0 prints "liftoff") โ€” then add the depth-printing version from the starter that indents by call depth. Run countdown(5) and watch the descent and return visually.
  2. Write naive fib(n) with a global call counter. Run fib(10), fib(20), fib(25) and record the call counts โ€” watch the explosion (fib(25) makes ~240k calls).
  3. Add @functools.lru_cache above fib, reset the counter, rerun fib(25) โ€” count the calls now (should be โ‰ค 49: each input once, plus lookups). Write the one-sentence explanation in a comment.
  4. Trigger the safety rail on purpose: call countdown(20_000) and read the RecursionError. Then state in a comment which conversion (plain loop) fixes it and why the loop has no such limit.
๐Ÿ python โ€” editable, runs in your browser
Ctrl/โŒ˜+Enter runs ยท Tab indents ยท numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)
guided 2

Recurse over real nested data + merge sort

20 min
  1. Write total_size(data): given arbitrarily nested lists of ints (e.g. [1, [2, [3, 4]], 5]), return the sum of all ints. Base case: an int. Recursive case: a list โ€” sum the recursion over its elements. This is the JSON-walking shape you will use forever.
  2. Write merge_sort from the tech section, plus the merge(left, right) combine using Day 24's two-pointer walk. Test against Python's sorted() on 100 random lists (starter shows the harness).
  3. In a comment, do the Day 22 accounting: levels ร— work-per-level = O(n log n). Then answer: what is merge sort's space complexity, and where does it go?
  4. Convert countdown to iteration two ways: a plain loop, and โ€” overkill here but the general tool โ€” an explicit stack of pending values (Day 25). The second version is the template for iterative tree traversal on Day 29.
๐Ÿ python โ€” editable, runs in your browser
Ctrl/โŒ˜+Enter runs ยท Tab indents ยท numpy/pandas/sklearn auto-load on import (torch and network calls need a local run)

On your own

Two solo

20 min

(1) Fast exponentiation: pow_fast(x, n) for non-negative n in O(log n) multiplications. The divide: x^n = (x^(n/2))ยฒ when n is even; odd n peels one factor. Verify against x ** n and count the multiplications for n = 1000 (should be ~15, not 1000).

(2) Flatten: flatten(data) returning a flat list from arbitrarily nested lists โ€” the total_size shape but building a list. Then state: what does the call stack's maximum depth depend on for this function (hint: nesting depth, not total length)?

Hints: for (1), recurse on n // 2 ONCE and square the result โ€” recursing twice rebuilds the fib explosion you just escaped.

Ship before you stop

Pattern log: the recursion section

Add "Recursion & Divide/Conquer" to patterns.md: the two-part contract (base case + shrinking step) as the checklist you run on every recursive function; your solved problems (nested sum, merge sort, fast pow, flatten) with one-line notes; the fib call-count table (naive vs memoized โ€” real numbers from your run); recognition cues ("nested", "defined in terms of itself", "sorted halves", "same problem, smaller"); and honest costs (stack depth = memory, ~1000-frame limit, when to convert to iteration + explicit stack). Close with the bridge sentence to Day 34, written in your own words: overlapping subproblems + cache = dynamic programming. Commit โ€” Week 4's toolkit is now complete for tomorrow's drill.

Rubric โ€” check what you completed (0/5)

Common mistakes & misconceptions

  • Writing the recursive case first and the base case as an afterthought. Write the base case FIRST โ€” it is the answer's foundation, and forgetting it is the #1 cause of RecursionError.
  • Recursing on an unshrunken input (fib(n) calling fib(n)). The base case only saves you if every call moves toward it โ€” check the shrinking step explicitly.
  • Treating RecursionError as "Python is broken." The ~1000-frame limit is a memory safety rail; deep linear recursions should be loops, deep branching ones explicit stacks.
  • Recursing twice on the same half in fast exponentiation (return pow_fast(x, n//2) * pow_fast(x, n//2)). That recomputation rebuilds the exponential call tree โ€” bind the result once, then square it.
  • Believing memoization is free. lru_cache trades O(n) memory (and hashable-argument requirements) for the speedup โ€” Day 22's space column applies.
  • Slicing lists in recursive calls without noticing the cost: nums[:mid] copies O(n) per level. Fine for learning merge sort; production versions pass index bounds instead.
Knowledge check

Q1. Naive recursive fib(40) is astronomically slower than fib(20). The root cause?

Q2. Merge sort is O(n log n). Where do the two factors come from?

Q3. A recursion must process a chain of 500,000 linked nodes. In Python you shouldโ€ฆ

Go deeper โ€” curated resources

bookOpenDSA โ€” recursion chapter (with visual call traces) โ†—25 mincourseCS50x Week 3 โ€” recursion & merge sort segments โ†—30 mintoolVisuAlgo โ€” sorting visualizer (watch merge sort's levels) โ†—10 min
If you have a third hour
  • Why Python has no tail-call optimization โ€” Some languages reuse the current frame for tail calls, making recursion loop-cheap. Python deliberately refuses (Guido values honest tracebacks). Knowing this explains why the "convert to a loop" rule is Python-specific advice.
Done means
  • Call-count explosion measured naive vs memoized and recorded
  • merge_sort passes 100 randomized tests against sorted()
  • pow_fast verified O(log n) by multiplication count
  • patterns.md recursion section committed; quiz โ‰ฅ 2/3
How this connects

โ† Back: The call stack is Day 25's structure run by the interpreter; the merge step is Day 24's two pointers on Day 26's problem; lru_cache is Day 12's decorator finally showing its full power.

Forward โ†’: Trees (Day 29) and graphs (Day 31) are traversed by exactly today's shapes. Day 32 builds on merge sort's accounting, Day 33 is divide-and-conquer with a discarded half, and Day 34 grows today's memoized-fib insight into dynamic programming proper.

Unlocks: D28 Week 4 Checkpoint: Pattern Drill ยท D29 Binary Trees & BSTs ยท D31 Graphs, BFS & DFS ยท D32 Sorting