Functions and the call stack · about 55 min

Recursion

Why this matters

Some problems are naturally described in terms of smaller versions of themselves. A directory contains files and directories. An arithmetic expression contains smaller expressions. To sort a list, sort each half and merge them. A function that calls itself, recursion, lets you write such solutions almost exactly as you would describe them. Recursion is how file-tree walkers, parsers, many sorting algorithms and tree data structures are written. In C it also has a very concrete cost, measured in stack frames, which you now know how to picture.

A function that calls itself

The factorial of n, written n!, is 1 x 2 x ... x n. It can be defined in terms of itself:

  n! = 1               if n <= 1     (base case)
  n! = n x (n - 1)!    otherwise     (recursive case)

That definition translates directly into C:

int factorial(int n)
{
    if (n <= 1)
        return 1;                    /* base case: no further calls */
    return n * factorial(n - 1);     /* recursive case: a smaller problem */
}
printf("5! = %d\n", factorial(5));   /* 5! = 120 */

Every correct recursive function has the same two parts:

  1. A base case that is answered directly, without recursion.
  2. A recursive case that calls the function on a smaller problem, one step closer to the base case, and uses the result.

If every recursive call moves toward the base case, the recursion ends. If not, it runs until the stack overflows.

What happens on the stack

There is nothing special about a function calling itself. Each call is an ordinary call with its own stack frame and its own n. Here is factorial(3):

 calls going down                          returns coming back up

 factorial(3): n=3, needs factorial(2)  ...   returns 3 * 2 = 6
   factorial(2): n=2, needs factorial(1) ...  returns 2 * 1 = 2
     factorial(1): n=1, base case        ...  returns 1

 stack at the deepest point:
 +-------------------------+
 | main                    |
 +-------------------------+
 | factorial  n=3          |  paused at  return n * factorial(n - 1)
 +-------------------------+
 | factorial  n=2          |  paused at the same line, different n
 +-------------------------+
 | factorial  n=1          |  running: returns 1
 +-------------------------+  <- top

Three frames, three separate variables called n, each waiting for the call above it to return. When factorial(1) returns 1, the frame for n=2 resumes, multiplies, returns 2, and so on back down to main.

On the way down and on the way back up

Where you put work relative to the recursive call decides when it happens:

void countdown(int n)
{
    if (n == 0) {
        printf("liftoff\n");
        return;
    }
    printf("%d\n", n);      /* before the call: on the way down */
    countdown(n - 1);
}
3
2
1
liftoff

Work placed after the recursive call happens as the calls return, in reverse order. That is handy for producing digits most-significant first:

void print_digits(int n)
{
    if (n >= 10)
        print_digits(n / 10);   /* first print everything except the last digit */
    printf("%d ", n % 10);      /* then the last digit */
}
print_digits(1234) prints: 1 2 3 4

A loop with n % 10 would naturally produce 4 3 2 1; the stack of pending calls reverses it for free.

Thinking recursively

The trick to writing recursive functions is to trust the recursive call. Do not trace every level in your head. Instead:

  1. Find the base case(s): the smallest inputs you can answer immediately.
  2. Assume the function already works for smaller inputs.
  3. Ask: given the answer for a smaller input, how do I build the answer for this one?

For the digit sum of 4096: assume sum_digits(409) correctly returns 13. Then the answer is 6 + 13. In code: n % 10 + sum_digits(n / 10), with base case sum_digits(0) == 0. Done.

The same reasoning solves the Towers of Hanoi in the exercises, a puzzle that is hard to solve with loops but takes four lines recursively.

The cost: exponential blowup

The Fibonacci numbers (0, 1, 1, 2, 3, 5, 8, ...) are defined as fib(n) = fib(n-1) + fib(n-2). The direct recursive version is beautiful and terribly slow:

long fib(int n)
{
    if (n < 2)
        return n;
    return fib(n - 1) + fib(n - 2);
}

Each call makes two more calls, and the same values are recomputed over and over: fib(5) computes fib(3) twice and fib(2) three times. Counting the calls:

fib(5) = 5, 15 calls
fib(10) = 55, 177 calls
fib(20) = 6765, 21891 calls
fib(30) = 832040, 2692537 calls
fib(40) = 102334155, 331160281 calls

Three hundred million calls for a number a loop computes in 40 steps:

long fib_loop(int n)
{
    long a = 0, b = 1;
    for (int i = 0; i < n; i++) {
        long next = a + b;
        a = b;
        b = next;
    }
    return a;
}

The lesson is not "recursion is slow" (factorial makes exactly n calls) but "count the calls". When a recursive function calls itself more than once on overlapping subproblems, the cost can explode.

Recursion or a loop?

Anything written recursively can be written with loops (and an explicit stack if necessary), and the reverse is also true. In C:

Some compilers turn a call in tail position (return f(n - 1, acc);, with nothing left to do afterwards) into a jump, using no extra stack. gcc does this at -O2, but the C standard does not require it, so do not rely on it.

Common mistakes

Recap

Going further: add a call counter to the naive fib and to fib_loop and compare for n = 35. Then write a recursive int power(int base, int exp) that uses power(base, exp / 2) once and squares it: it needs only about log2(exp) calls.

Exercises

Exercise 1 · quiz

Before or after the call?

What does show(3) print?

void show(int n)
{
    if (n == 0)
        return;
    show(n - 1);
    printf("%d ", n);
}
Answer
  • 1 2 3

The printf comes after the recursive call, so each call first lets all the smaller calls finish. show(3) calls show(2), which calls show(1), which calls show(0), which returns at once. Then show(1) prints 1 and returns, show(2) prints 2, show(3) prints 3. Work done before the recursive call happens on the way down; work done after it happens on the way back up, in reverse order.

Exercise 2 · write code

Sum of digits, recursively

Write a recursive function

int sum_digits(int n)

that returns the sum of the decimal digits of a non-negative integer, for example sum_digits(4096) is 4 + 0 + 9 + 6 = 19, and sum_digits(0) is 0. Do not use any loop (for, while, do): the check rejects them. Think of n % 10 (the last digit) and n / 10 (all the other digits).

File: main.c

Starting point

#include <stdio.h>

int sum_digits(int n)
{
    return n % 10;
}

int main(void)
{
    printf("%d\n", sum_digits(4096));
    return 0;
}
Hint 1

Base case: the digit sum of 0 is 0.

Hint 2

Recursive case: last digit plus the digit sum of the rest: n % 10 + sum_digits(n / 10).

Reference solution
#include <stdio.h>

int sum_digits(int n)
{
    if (n == 0)
        return 0;
    return n % 10 + sum_digits(n / 10);
}

int main(void)
{
    printf("%d\n", sum_digits(4096));
    return 0;
}

Exercise 3 · write code

Print in binary

Read a non-negative integer and print it in binary (base 2), followed by a newline. For example 13 prints 1101, 1 prints 1, and 0 prints 0.

Write a recursive function void print_binary(int n) that prints the digits most significant first. Repeatedly taking n % 2 gives the bits in the wrong order (least significant first); recursion fixes that if you print after the recursive call.

File: main.c

Starting point

#include <stdio.h>

void print_binary(int n)
{
    printf("%d", n % 2);
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n < 0)
        return 1;
    print_binary(n);
    printf("\n");
    return 0;
}

Example

input

13

expected output

1101

Example

input

0

expected output

0
Hint 1

n / 2 is n with its last bit removed; n % 2 is the last bit.

Hint 2

Print the higher bits first by recursing on n / 2, then print n % 2.

Hint 3

Stop recursing when n < 2, so that 0 and 1 print a single digit. if (n >= 2) print_binary(n / 2);

Reference solution
#include <stdio.h>

void print_binary(int n)
{
    if (n >= 2)
        print_binary(n / 2);
    printf("%d", n % 2);
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n < 0)
        return 1;
    print_binary(n);
    printf("\n");
    return 0;
}

Exercise 4 · quiz

Counting calls

With the naive definition

long fib(int n)
{
    if (n < 2)
        return n;
    return fib(n - 1) + fib(n - 2);
}

how many calls to fib in total (including the first) does fib(5) make?

Answer
  • 15

calls(n) = 1 + calls(n-1) + calls(n-2), with calls(0) = calls(1) = 1. That gives 1, 1, 3, 5, 9, 15 for n = 0..5. The count grows roughly like 1.6^n: fib(30) makes about 2.7 million calls and fib(40) about 331 million, because the same subproblems are recomputed again and again. A simple loop that keeps the last two values computes fib(40) in 40 steps.

Exercise 5 · write code

Towers of Hanoi

Three pegs, A, B and C. Peg A holds n disks, largest at the bottom. Move them all to peg C, one disk at a time, never putting a larger disk on a smaller one.

The recursive insight: to move n disks from A to C, move the top n - 1 disks out of the way to B (using C as the spare), move disk n from A to C, then move the n - 1 disks from B onto C (using A as the spare). Moving 0 disks does nothing.

Read n and print every move, one per line, in exactly this format:

move disk 1 from A to B

For n = 2 the output is:

move disk 1 from A to B
move disk 2 from A to C
move disk 1 from B to C

Write a function void hanoi(int n, char from, char to, char via).

File: main.c

Starting point

#include <stdio.h>

void hanoi(int n, char from, char to, char via)
{
    /* base case, then three steps */
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n < 0)
        return 1;
    hanoi(n, 'A', 'C', 'B');
    return 0;
}

Example

input

1

expected output

move disk 1 from A to C

Example

input

2

expected output

move disk 1 from A to B
move disk 2 from A to C
move disk 1 from B to C
Hint 1

Base case: if n is 0, return.

Hint 2

Step 1 moves n - 1 disks from from to via, so the spare for that call is to: hanoi(n - 1, from, via, to);

Hint 3

Then print the move of disk n from from to to, then hanoi(n - 1, via, to, from);

Reference solution
#include <stdio.h>

void hanoi(int n, char from, char to, char via)
{
    if (n == 0)
        return;
    hanoi(n - 1, from, via, to);
    printf("move disk %d from %c to %c\n", n, from, to);
    hanoi(n - 1, via, to, from);
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1 || n < 0)
        return 1;
    hanoi(n, 'A', 'C', 'B');
    return 0;
}