1.2.37 · D4Introduction to Programming (Python)

Exercises — Recursion — call stack visualization, base case, recursive case

2,271 words10 min readBack to topic

Level 1 — Recognition

Goal: identify the base case, the recursive case, and whether progress is made — without running anything.

Exercise 1.1

Look at this function. Name the base case line and the recursive case line, and state which input variable shrinks.

def mystery(k):
    if k == 1:
        return "hi"
    return mystery(k - 1)
Recall Solution 1.1
  • Base case: if k == 1: return "hi" — an input (k == 1) whose answer is returned with no further call.
  • Recursive case: return mystery(k - 1) — the function calls itself.
  • Shrinking variable: k, because each call passes k - 1 (one smaller).

What it looks like: a staircase going down from k toward 1. The picture below shows the frames stacking for mystery(4).

Figure — Recursion — call stack visualization, base case, recursive case

Exercise 1.2

Which of these has no valid base case (will crash)? Explain in one sentence.

# A
def a(n):
    if n == 0: return 0
    return n + a(n - 1)
 
# B
def b(n):
    return n + b(n - 1)
Recall Solution 1.2

B crashes. a catches n == 0 and returns without recursing, so it stops. b has no if that returns without calling itself — every call makes another call forever → RecursionError: maximum recursion depth exceeded.


Level 2 — Application

Goal: run the machine by hand — trace the winding and unwinding.

Exercise 2.1

Trace fact(4) using

def fact(n):
    if n == 0: return 1
    return n * fact(n-1)

Give the final number and the order in which the four calls return.

Recall Solution 2.1

Winding (push): fact(4)fact(3)fact(2)fact(1)fact(0). Base: fact(0) returns first (deepest frame, LIFO). Unwinding (pop, multiply on the way up):

  • fact(1):
  • fact(2):
  • fact(3):
  • fact(4):

Return order: fact(0), then fact(1), fact(2), fact(3), fact(4). Answer .

Exercise 2.2

Trace power(2, 3) (compute ):

def power(base, exp):
    if exp == 0: return 1
    return base * power(base, exp - 1)

Give the result and how many frames sit on the stack at the deepest point.

Recall Solution 2.2

Descent shrinks exp: .

  • power(2,0) = (base)
  • power(2,1) =
  • power(2,2) =
  • power(2,3) =

Deepest stack: frames for exp = 3, 2, 1, 0 are all alive at once → 4 frames. In general needs exp + 1 frames.

Exercise 2.3

Trace summ([5, 3, 8, 1]) using the list-sum from the parent note. Give the result.

Recall Solution 2.3

Each call peels off the head L[0] and recurses on the tail L[1:]: Building back up: , , , . Answer .


Level 3 — Analysis

Goal: reason about depth, break-points, and why order matters.

Exercise 3.1

Python's default recursion limit is about 1000 frames. This function is called as deep(0):

def deep(n):
    if n == 1000: return n
    return deep(n + 1)

Roughly how many frames stack up before it stops, and does it stop or crash?

Recall Solution 3.1

n climbs . It stops at n == 1000 — that's the base case. Frames alive at the deepest point: one for each of , i.e. 1001 frames. Because , this exceeds the default limit and raises RecursionError before reaching the base case. (See Big-O and Recursion Depth — depth, not just correctness, matters.)

Exercise 3.2

The two functions differ only in where print sits. State what each prints for input 3.

def down(n):              # A
    if n < 0: return
    print(n)
    down(n - 1)
 
def up(n):                # B
    if n < 0: return
    up(n - 1)
    print(n)
Recall Solution 3.2
  • A (down) prints during winding (before the call): 3, 2, 1, 0.
  • B (up) prints during unwinding (after the call returns): 0, 1, 2, 3.

Why: in B, each frame first fully finishes its child call, and only then runs print(n). The deepest frame (n = 0) unwinds first, so 0 prints first, then 1, and so on back up. Same recursion, mirror-image output.

Exercise 3.3

Does this crash or return? If it returns, what does f(10) give?

def f(n):
    if n == 0: return 0
    return f(n - 2)
Recall Solution 3.3

Starting at 10 and subtracting 2: → hits the base case n == 0, returns , and every frame passes that back unchanged → returns 0. Danger: if called with an odd number like f(9), it would step and jump past 0 forever → crash. The base case must be reachable by the step size, not just "smaller."


Level 4 — Synthesis

Goal: design your own recursions from a self-similarity insight.

Exercise 4.1

Write a recursive length(L) that returns how many items a list has, without using len(). Then give length([9, 9, 9]).

Recall Solution 4.1

Self-similarity: the length of a list is (for the head) plus the length of the tail. Base case: the empty list has length .

def length(L):
    if not L:               # base case
        return 0
    return 1 + length(L[1:])  # recursive case

length([9,9,9]).

Exercise 4.2

Write a recursive reverse(s) that reverses a string. Trace reverse("cat").

Recall Solution 4.2

Self-similarity: reversing a string = reverse of its tail, with the head stuck on the end. Base case: an empty string reversed is "".

def reverse(s):
    if s == "":                 # base case
        return ""
    return reverse(s[1:]) + s[0]  # tail reversed, then head at the back

Trace reverse("cat"):

  • reverse("at") + "c"
  • (reverse("t") + "a") + "c"
  • ((reverse("") + "t") + "a") + "c"
  • ((("" + "t") + "a") + "c") = "t" + "a" + "c" = "tac"

Exercise 4.3

Write a recursive gcd(a, b) (greatest common divisor) using Euclid's rule , with base case . Compute gcd(48, 18).

Recall Solution 4.3

Why this recursion: Euclid noticed the gcd of two numbers equals the gcd of the smaller one and the remainder — a strictly smaller pair each step, guaranteeing we reach b == 0.

def gcd(a, b):
    if b == 0:            # base case
        return a
    return gcd(b, a % b)  # recursive case, remainder shrinks

Trace gcd(48, 18):

  • gcd(48, 18)gcd(18, 48 % 18 = 12)
  • gcd(18, 12)gcd(12, 18 % 12 = 6)
  • gcd(12, 6)gcd(6, 12 % 6 = 0)
  • gcd(6, 0) → base case → returns 6.

Level 5 — Mastery

Goal: combine recursion with tree-shaped or overlapping structure.

Exercise 5.1

fib computes Fibonacci with . This is two recursive calls per frame — a tree, not a line. Draw/reason the call tree for fib(4) and give its value and how many times fib(1) is evaluated.

Recall Solution 5.1

The tree branches twice each step (see figure). Values: .

Figure — Recursion — call stack visualization, base case, recursive case

Counting the leaves that equal fib(1): expanding fully, fib(1) is reached 3 times and fib(0) 2 times. That repeated work is exactly the "overlapping subproblems" problem — see Fibonacci and overlapping subproblems and its cure, Memoization and Dynamic Programming.

Exercise 5.2

Write a recursive count_nodes(tree) that counts every number in a nested list like [1, [2, 3], [4, [5, 6]]] (each element is either a number or another list). Give the count for that example.

Recall Solution 5.2

Self-similarity: a nested list's count = sum of the counts of its elements; a number counts as ; a list is counted by the same function (recursion inside the loop). This is the shape of Tree and Graph Traversal.

def count_nodes(x):
    if not isinstance(x, list):   # base case: a plain number
        return 1
    total = 0
    for elem in x:                # recursive case: sum children
        total += count_nodes(elem)
    return total

For [1, [2, 3], [4, [5, 6]]] the numbers are 6.

Exercise 5.3

Compare, for fib(20), the number of function calls made by the plain recursion versus a memoized version that stores each answer once. (You don't need the exact plain count — reason about the shape.)

Recall Solution 5.3
  • Memoized: each distinct fib(k) for is computed once → about 21 stored results (each further lookup is instant). This is Memoization and Dynamic Programming turning a tree into a line.
  • Plain recursion: the call tree roughly doubles each level; the exact total number of calls to compute fib(20) is calls.

So memoization turns ~21891 calls into ~21 computations — the payoff grows explosively with n. Compare with plain Iteration — for and while loops, which also does it in ~20 steps but without a stack.


Recall Final self-check

Cover the answers and say each aloud. Return order of fact(4)'s calls? ::: fact(0), fact(1), fact(2), fact(3), fact(4). Frames alive at deepest point of power(2,3)? ::: 4 (exp = 3,2,1,0). reverse("cat") = ? ::: "tac". gcd(48,18) = ? ::: 6. fib(4) = ? and how many fib(1) evaluations? ::: 3, and fib(1) is evaluated 3 times. Why does f(n)=f(n-2) crash on odd n but not even? ::: Odd starts skip past the n==0 point; use n<=0 for a reachable range.