computer-science/recursion-why-fib-30-makes-2-7-million-calls-and-how-a-notebook-fixes.md

Recursion: why fib(30) makes 2.7 million calls, and how a notebook fixes it

Computer science · 5 min read ·

A function that calls itself sounds like an endless loop, but it works when every call gets a smaller problem and one case is simple enough to answer directly.

Step through the calls of a Fibonacci function, see why computing fib(30) takes 2,692,537 calls, and watch a simple notebook of remembered answers cut that to 59.

A tree of circles showing every recursive call to fib(5), with repeated calls highlighted in orange

Here is a trick that surprises many beginners: a function is allowed to call itself. This is called recursion. It sounds like a loop that never ends, but it works as long as each call gets a slightly smaller problem and there is a point where the calls stop.

Two ingredients

Every recursive function has two parts. The base case is the situation so small that the answer is known right away. The recursive case solves a slightly smaller version of the same problem by calling the function again, then uses that answer. Here is the factorial of a number (5! means 5 × 4 × 3 × 2 × 1):

def fact(n):
    if n == 0:          # base case: stop here
        return 1
    return n * fact(n - 1)   # recursive case: a smaller problem

print(fact(5))   # 120

When you call fact(5), it needs fact(4), which needs fact(3), and so on down to fact(0), which simply returns 1. Then each waiting call finishes in turn: 1, then 1 × 1, 2 × 1, 3 × 2, 4 × 6, and finally 5 × 24 = 120. Python keeps track of the waiting calls on the call stack, a pile where each unfinished call sits until the call above it is done.

What happens without a base case

If the calls never reach the base case, the pile grows and grows. Python protects you by stopping at a limit (1000 calls deep by default in standard Python):

def forever(n):
    return forever(n + 1)

forever(0)
# RecursionError: maximum recursion depth exceeded

So the first question to ask about any recursive function is: does every path end at the base case?

When a function calls itself twice

Factorial calls itself once, so its calls form a single chain. Now take the Fibonacci numbers, where each number is the sum of the two before it: 0, 1, 1, 2, 3, 5, 8, 13... The definition is already recursive: fib(n) = fib(n-1) + fib(n-2), with fib(0) = 0 and fib(1) = 1 as base cases.

calls = 0
def fib(n):
    global calls
    calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(6), calls)   # 8 25

Computing fib(6) gave the right answer, 8, but it took 25 calls for such a small number. Because each call makes two more calls, the calls branch into a tree. Try it yourself below. Press Step to watch the calls happen in order, one at a time. The filled circle is the call running right now.

Look at the tree with n = 5. The value fib(3) is worked out twice, from scratch, and fib(2) three times. The function has no memory, so it repeats work it has already done. Move the slider up and the tree grows fast: with n = 30 the real count is 2,692,537 calls for an answer that has only 31 different questions in it.

Fix: write answers down

The cure is simple. Keep a notebook, which in Python is a dictionary, and write each answer in it. Before doing any work, look in the notebook first. This idea is called memoization (from "memo", a note to remember).

memo = {}
calls = 0
def fib(n):
    global calls
    calls += 1
    if n in memo:         # already solved? use the note
        return memo[n]
    if n < 2:
        return n
    memo[n] = fib(n - 1) + fib(n - 2)
    return memo[n]

print(fib(6), calls)   # 8 11

Now click Remember answers in the figure above and step through again. The repeated branches shrink to a single dashed circle, because the answer is already written down. I ran this code for a few sizes and counted the calls:

nWithout notebookWith notebook
1017719
2021,89139
302,692,53759

Without the notebook, the work roughly multiplies by 1.6 each time n goes up by one. With it, the work grows by two calls per step. Same answer, wildly different cost.

What to take into your own code

When you write a recursive function, check three things: a base case exists, each call moves toward it, and the same smaller problem is not being solved again and again. If it is, a dictionary of past answers is often all you need. Python also ships a ready-made notebook: put @functools.cache above a function and it remembers results for you.

Recursion is not always the best tool. A plain loop is fine for simple counting, and very deep recursion can hit the limit. But for problems that split into smaller copies of themselves, such as folders inside folders or branching choices, it is often the clearest way to say what you mean.

$ ls computer-science/

see all →
computer-science/merge-sort-how-splitting-a-list-in-half-again-and-again-sorts-it-with.md Merge sort: how splitting a list in half again and again sorts it with far less work How do you sort 10,000 numbers without making millions of comparisons? Merge sort never sorts a big pile. It splits the list until every… 5 min read · 27 Oct computer-science/hash-tables-how-a-computer-finds-one-name-among-thousands-without-chec.md Hash tables: how a computer finds one name among thousands without checking them all Your phone finds one contact among thousands without reading them all. The trick is a hash table: a row of numbered slots and a small… 6 min read · 24 Oct computer-science/stacks-and-queues-why-undo-takes-the-newest-item-and-a-printer-takes-t.md Stacks and queues: why undo takes the newest item and a printer takes the oldest Press undo and the last thing you typed disappears first. Send documents to a shared printer and the first one is printed first. Both are… 5 min read · 20 Oct computer-science/breadth-first-search-how-a-maze-solver-finds-the-shortest-way-out.md Breadth-first search: how a maze solver finds the shortest way out How does a maze solver, or a map app, find the shortest route and not just any route? The answer is breadth-first search: explore every… 6 min read · 17 Oct computer-science/binary-search-how-to-find-one-number-in-a-million-with-20-guesses.md Binary search: how to find one number in a million with 20 guesses Guessing a number between 1 and 1,000,000 sounds like it could take a million tries. With binary search it takes at most 20. Step through a… 4 min read · 14 Oct computer-science/big-o-how-to-tell-if-your-code-will-survive-1-000-times-more-data.md Big O: how to tell if your code will survive 1,000 times more data Your code works on 10 items, then crawls on 10,000. Big O is the short way programmers describe how the number of steps grows as the input… 4 min read · 11 Oct