All A Level 1 45 min read 10 question quiz 20 exam marks

Recursion

Functions that call themselves: the base case, the call stack and why it can overflow, memoisation, Tower of Hanoi and a backtracking maze solver, with every example runnable.

Recursion and the call stack

A recursive subprogram is one that calls itself. It solves a problem by solving a smaller version of the same problem, and it needs two things:

  • a base case, a version of the problem small enough to answer directly, with no further call
  • a recursive case, which does a little work and calls itself with an argument that is closer to the base case
Python
def factorial(n):
    if n <= 1:                     # base case
        return 1
    return n * factorial(n - 1)    # recursive case

print(factorial(5))                # 120

factorial(4) cannot give its answer until factorial(3) has, and that waits on factorial(2). The answer is built as the calls unwind:

factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * 1))
= 24

The call stack

Every call needs somewhere to keep its own parameters, local variables and the place to return to. The computer keeps these in a stack frame, and the frames are held on the call stack. A call pushes a frame. A return pops one.

The call stack during recursionThe call stack while factorial of 4 runs. Calling factorial 4 pushes a frame holding n equals 4. That call needs factorial 3, so a second frame is pushed on top. Factorial 3 calls factorial 2, which adds a third frame, and factorial 2 calls factorial 1, the base case, which makes the stack four frames deep. The returns then happen in reverse: factorial 1 returns 1 and its frame is popped, factorial 2 returns 2 times 1 which is 2, factorial 3 returns 3 times 2 which is 6, and factorial 4 returns 4 times 6 which is 24. Each call has its own frame, with its own n, which is why the stack grows with the depth of the recursion.The call stack while factorial(4) runsbottom of the stackcalls pushreturns popfactorial(4)n = 4, waiting for factorial(3)1factorial(4)n = 4, waiting for factorial(3)factorial(3)n = 3, waiting for factorial(2)12factorial(4)n = 4, waiting for factorial(3)factorial(3)n = 3, waiting for factorial(2)factorial(2)n = 2, waiting for factorial(1)123factorial(4)n = 4, waiting for factorial(3)factorial(3)n = 3, waiting for factorial(2)factorial(2)n = 2, waiting for factorial(1)factorial(1)n = 1, the base case: no further call1234factorial(4)n = 4, waiting for factorial(3)factorial(3)n = 3, waiting for factorial(2)factorial(2)n = 2, waiting for factorial(1)12345returns 1factorial(4)n = 4, waiting for factorial(3)factorial(3)n = 3, waiting for factorial(2)12345returns 16returns 2 × 1 = 2factorial(4)n = 4, waiting for factorial(3)12345returns 16returns 2 × 1 = 27returns 3 × 2 = 612345returns 16returns 2 × 1 = 27returns 3 × 2 = 68returns 4 × 6 = 24
Nothing is returned until the base case is reached, and the answer is built on the way back down.

This version prints each call and each return, indented by how deep it is, so you can watch the stack grow and shrink:

Python
def factorial(n, depth=0):
    print("  " * depth + "call factorial(" + str(n) + ")")
    if n <= 1:
        print("  " * depth + "base case, return 1")
        return 1
    result = n * factorial(n - 1, depth + 1)
    print("  " * depth + "return " + str(result))
    return result

factorial(4)

Without a base case

If the recursion never reaches a base case, frames pile up until the stack is full. Python stops it at about a thousand frames and raises a RecursionError. Other languages call the same event a stack overflow.

Python
# raises: RecursionError
def runaway(n):
    return runaway(n + 1)

runaway(0)
The base case is not enough on its own

Every recursive call must move towards it. factorial(n) that calls factorial(n + 1) has a perfectly good base case and will never reach it.

More small examples

The same pattern, a base case and a smaller problem, solves many things. In each, find the base case first.

Python
def sum_list(items):
    if items == []:
        return 0
    return items[0] + sum_list(items[1:])     # the head, plus the sum of the tail

def reverse(text):
    if len(text) <= 1:
        return text
    return reverse(text[1:]) + text[0]

def gcd(a, b):
    if b == 0:
        return a
    return gcd(b, a % b)

print(sum_list([4, 8, 15, 16]))      # 43
print(reverse("recursion"))          # noisrucer
print(gcd(48, 18))                   # 6

sum_list splits a list into its head (items[0]) and its tail (items[1:]). You will meet that pair again in functional programming.

The cost of recursion, and how to cut it

Recursion is elegant and it is not free. The call stack takes memory, and the naive version of some recursive functions repeats the same work again and again.

The Fibonacci trap

Each Fibonacci number is the sum of the two before it, so the definition is recursive. The code writes itself:

Python
calls = 0

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

print(fib(20), calls)       # 6765 21891

Twenty one thousand calls to get an answer that needs twenty additions. fib(5) calls fib(3) and fib(4), and fib(4) calls fib(3) again. The tree of calls roughly doubles with each step, so the work grows as 2 to the power of n.

Memoisation

Memoisation stores each result the first time it is worked out, so a repeat call is answered from the store. Python's functools.lru_cache does it with one line.

Python
from functools import lru_cache

calls = 0

@lru_cache(maxsize=None)
def fib(n):
    global calls
    calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(80), calls)       # 23416728348467685 81

fib(80) would take longer than a human lifetime without the cache. With it, the function body runs just 81 times, once for each value from 0 to 80. This trades memory for time.

Recursion or iteration

Anything recursion can do, a loop can do, and the reverse. Choosing between them is a judgement.

RecursionIteration
Code for tree shaped or self similar problemsShort and close to the definitionNeeds a stack you manage yourself
MemoryOne stack frame per callA fixed amount
SpeedA call has overheadFaster
RiskRecursionError if too deepNone
Easy to traceHarderEasier
Python
def factorial_loop(n):
    result = 1
    for i in range(2, n + 1):
        result = result * i
    return result

print(factorial_loop(5))     # 120

In Python the limit matters in practice, because the language does not optimise a call that is the last thing a function does:

Python
# raises: RecursionError
def depth(n):
    if n == 0:
        return 0
    return 1 + depth(n - 1)

print(depth(900))
print(depth(5000))
Which to choose

Prefer recursion when the problem is naturally made of smaller copies of itself, such as a tree, a file system, or splitting a list in half. Prefer a loop when the problem is simply "repeat this n times", or when n could be large.

Problems that recurse: Hanoi, nesting and backtracking

Tower of Hanoi

Three pegs and a stack of disks, largest at the bottom. Move the whole stack to another peg, one disk at a time, never putting a bigger disk on a smaller one. The recursive idea: to move n disks, move the top n - 1 out of the way, move the biggest, then move the n - 1 back on top.

Python
def hanoi(n, source, target, spare):
    if n == 0:
        return 0
    moves = hanoi(n - 1, source, spare, target)
    print("move disk", n, "from", source, "to", target)
    moves = moves + 1
    moves = moves + hanoi(n - 1, spare, target, source)
    return moves

total = hanoi(3, "A", "C", "B")
print(total, "moves")       # 7 moves

Each extra disk doubles the number of moves and adds one: n disks need 2 to the n minus 1. Sixty four disks, the legend's number, needs 18 quintillion moves.

Nested data

A loop cannot easily handle lists inside lists inside lists, because you do not know how deep it goes. Recursion does not need to.

Python
def flatten(items):
    result = []
    for item in items:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result

print(flatten([1, [2, [3, 4]], [5], 6]))     # [1, 2, 3, 4, 5, 6]

Backtracking

Backtracking tries a choice, carries on, and if it reaches a dead end it undoes the choice and tries another. It is a recursive search, and the path.pop() below is the undo.

Python
maze = [
    "S.#.....",
    ".##.###.",
    "....#...",
    ".####.#.",
    "......#E",
]

def solve(row, col, path, seen):
    if not (0 <= row < len(maze) and 0 <= col < len(maze[0])):
        return False
    if maze[row][col] == "#" or (row, col) in seen:
        return False
    seen.add((row, col))
    path.append((row, col))
    if maze[row][col] == "E":
        return True
    for step_row, step_col in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
        if solve(row + step_row, col + step_col, path, seen):
            return True
    path.pop()                  # dead end: undo this square and back up
    return False

path = []
seen = set()
if solve(0, 0, path, seen):
    grid = [list(line) for line in maze]
    for row, col in path[1:-1]:
        grid[row][col] = "*"
    for line in grid:
        print("".join(line))
    print("path of", len(path), "squares; tried", len(seen), "to find it")

The solver tries right, then down, then left, then up. At the start it goes right, finds a wall, and backs up. The gap between the two numbers printed is the squares it tried and had to abandon.

State the base case, and say why it works

Every recursion question awards a mark for the base case and a mark for showing that the recursive call gets closer to it. Say both, in words, before you write the code: "when n is 0 return 1, and each call reduces n by one".

Solving the Towers of Hanoi with three discs by recursion, and how the number of moves growsThe Towers of Hanoi puzzle with three pegs, A, B and C, and three discs, solved by recursion. At the start all three discs are on peg A. Step one moves the top two discs to peg B, which is a smaller version of the same problem. Step two moves the biggest disc to peg C. Step three moves the two discs from B onto it. To move n discs you move n minus 1 discs out of the way, move the biggest disc, then move the n minus 1 discs back on top, and the base case is that one disc is simply moved. The number of moves is 2 to the power n minus 1: 1, 3, 7, 15, 31 and 63 moves for 1 to 6 discs, so each extra disc doubles the work.1StartABCall three discs on peg A2Step 1ABCtop 2 discs to peg B3Step 2ABCbiggest disc to peg C4Step 3ABC2 discs back on topThe recursive idea1Move the top n - 1 discs to the spare peg.a smaller version of the same problem2Move the biggest disc to the target peg.one move3Move the n - 1 discs on top of it.the same method againBase case: with 1 disc, just move it. That stops the recursion.Moves for n discs113273154315636moves = 2^n - 1each extra disc doubles the work
  1. Start: all the discs are on the first peg.
  2. Move the top n - 1 discs to the spare peg. That is the same problem, but smaller.
  3. Move the biggest disc to the target peg.
  4. Move the n - 1 discs on top of it, using the same method again.
A recursive solution needs a base case that stops it, and a step that makes the problem smaller.

Key terms you must be able to define

Recursion
A technique in which a subprogram solves a problem by calling itself with a smaller version of the same problem.
Base case
The case in a recursive subprogram that is answered directly, without another recursive call, and which stops the recursion.
Recursive case
The case in a recursive subprogram that makes a call to itself, with an argument closer to the base case.
Call stack
The area of memory that holds a stack frame for each active call, with its parameters, locals and return address.
Stack frame
The block of the call stack belonging to one call of a subprogram.
Stack overflow
An error when the call stack runs out of space, usually from recursion that never reaches its base case.
Memoisation
Storing the result of each call so that a repeated call with the same argument is answered without recalculating.
Backtracking
Trying a choice, and undoing it to try another if it leads to a dead end.
Divide and conquer
Splitting a problem into smaller copies of itself, solving each and combining the results.

Flashcards

Say the definition out loud before you flip the card. Retrieving it from memory is what builds recall, reading it again is not.

DefineRecursion
AnswerA technique in which a subprogram solves a problem by calling itself with a smaller version of the same problem.
DefineBase case
AnswerThe case in a recursive subprogram that is answered directly, without another recursive call, and which stops the recursion.
DefineRecursive case
AnswerThe case in a recursive subprogram that makes a call to itself, with an argument closer to the base case.
DefineCall stack
AnswerThe area of memory that holds a stack frame for each active call, with its parameters, locals and return address.
DefineStack frame
AnswerThe block of the call stack belonging to one call of a subprogram.
DefineStack overflow
AnswerAn error when the call stack runs out of space, usually from recursion that never reaches its base case.
DefineMemoisation
AnswerStoring the result of each call so that a repeated call with the same argument is answered without recalculating.
DefineBacktracking
AnswerTrying a choice, and undoing it to try another if it leads to a dead end.

Reach the top grade on this topic

Recursion is examined as tracing, writing and judging. The top marks come from precision about the stack.

  • Identify the base case and the recursive case separately, and say that each call moves towards the base case
  • When tracing, write the calls going down and then the values returning back up
  • Explain a stack overflow as the stack frames using up the available memory
  • In a comparison, give a cost and a benefit of each: memory and speed against clarity
  • Recognise when memoisation applies: the same arguments are being recalculated

Mistakes that cost marks here

Avoid these
  • Writing a recursive case but no base case, or a base case that is never reached.
  • Tracing only the way down and forgetting that nothing is returned until the base case is reached.
  • Saying recursion is always slower, or always better, without a reason tied to the problem.
  • Thinking each call shares the same local variables, when each has its own frame.
  • Forgetting to return the result of the recursive call, so the function gives back None.
Fun fact

Recursion is so central to how computers are described that a classic joke has the dictionary entry for it read: recursion, see recursion. LISP, designed in 1958, was the first widely used language built around functions that call themselves.

Knowledge check

0 of 10 answered
1What two parts must every recursive subprogram have?
2What happens when a recursive function has no reachable base case?
3What does factorial(4) return, given factorial(n) returns 1 for n <= 1, otherwise n * factorial(n - 1)?
4Where does the computer store the parameters and return address of each call?
5Which is a disadvantage of recursion compared with iteration?
6In gcd(a, b), which returns a when b == 0 and otherwise gcd(b, a % b), what is the base case?
7Why is the naive recursive Fibonacci function slow for large n?
8What does memoisation do?
9How many moves does the Tower of Hanoi need for 4 disks?
10Which technique tries a path and undoes it if it reaches a dead end?
Score 0 / 10

Exam-style questions

Five written questions in the style of the real paper. Write a full answer, then let the marker check it against the mark scheme.

20 marks
1State what is meant by the base case of a recursive subprogram, and explain why every recursive subprogram needs one.3 marks
State

Mark scheme0 / 3
  • LThe base case is the case where the answer is returned without a further recursive call
  • LWithout a base case the subprogram would keep calling itself forever
  • LThis fills the call stack, causing a stack overflow
Model answer

The base case is the version of the problem that is small enough to be answered directly, so it returns a value without making another recursive call. Every recursive subprogram needs one because otherwise it would keep calling itself with no way to stop. Each call adds a stack frame, so the call stack would eventually be filled and the program would fail with a stack overflow.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
2The subprogram shown is called with mystery(4). Show the sequence of calls, and the values that are returned, and state the value that is output.4 marks
Show
Python
def mystery(n):
    if n == 0:
        return 0
    return n + mystery(n - 1)

print(mystery(4))

Mark scheme0 / 4
  • LIdentifies that mystery(0) is the base case and returns 0
  • LShows the chain of calls from mystery(4) down to mystery(0)
  • LShows the returns unwinding, 0, 1, 3, 6, 10
  • LStates that the output is 10
Model answer

mystery(4) calls mystery(3), which calls mystery(2), then mystery(1), then mystery(0). mystery(0) is the base case and returns 0. The calls then return: mystery(1) returns 1 + 0 = 1, mystery(2) returns 2 + 1 = 3, mystery(3) returns 3 + 3 = 6 and mystery(4) returns 4 + 6 = 10. The value output is 10, which is the sum of the numbers from 1 to 4.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
3Write a recursive function called power that takes a number, base, and a whole number, exponent, that is 0 or more, and returns base raised to the power exponent. You must not use the ** operator or a loop.4 marks
Write

Mark scheme0 / 4
  • LDefines a function with the two parameters
  • LHas a base case: when exponent is 0 the result is 1
  • LHas a recursive case that multiplies base by power of exponent - 1
  • LReturns the result of the recursive case
Model answer
Python
def power(base, exponent):
    if exponent == 0:
        return 1
    return base * power(base, exponent - 1)

print(power(2, 10))

When the exponent is 0 the answer is 1, which is the base case. Otherwise the function multiplies base by base to the power one less, so each call moves the exponent closer to 0. power(2, 10) returns 1024.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
4Explain what a stack overflow is, and how it can happen in a program that uses recursion.3 marks
Explain

Mark scheme0 / 3
  • LEach call to a subprogram adds a stack frame to the call stack
  • LThe call stack has a limited size
  • LA recursive subprogram with no reachable base case, or too deep a recursion, uses up all of it
Model answer

Each time a subprogram is called, a stack frame holding its parameters, local variables and return address is added to the call stack, and it is only removed when the call returns. The call stack has a limited size. If a recursive subprogram never reaches its base case, or the recursion is simply too deep, the frames use up all the space available and the program fails with a stack overflow.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
5A programmer must write a subprogram to total the numbers in a list. Discuss whether the programmer should use recursion or iteration.6 marks
Discuss

Mark scheme0 / 6
  • LIteration uses a fixed amount of memory however long the list is
  • LRecursion adds a stack frame for every item, so a long list could cause a stack overflow
  • LA loop is quicker because there is no overhead of calling a subprogram repeatedly
  • LA loop is simple and easy to trace and debug for this problem
  • LRecursion could be shorter for a nested or tree shaped list
  • LA reasoned conclusion: iteration is the better choice for a flat list
Model answer

For a simple list of numbers iteration is the better choice. A loop uses a fixed amount of memory, whereas a recursive solution adds a stack frame for each item, so a very long list could fill the call stack and cause a stack overflow. A loop is also quicker, because it avoids the overhead of making a subprogram call for every item, and it is simple to trace and debug. Recursion has the advantage of being shorter and closer to the definition of the problem, which would make it a better choice if the list were nested to unknown depth or tree shaped. Since the list here is flat, the programmer should use iteration.

The marker looks for key phrases. If you said it another way and it is still right, give yourself the mark.
Machine mark 0 / 20