6.8. Recursion

Time: 00:04:04 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)

The chapter began with an illustration suggesting that functions can call one another, forming a sequence or chain of function calls. The sequence length can be quite long, limited only by the program's memory, and the functions return in the opposite order of their calls. Recursion describes the case where a sequence of function calls form a cycle. It's easier to understand and classify recursion with two pseudo-code examples.

Direct RecursionIndirect Recursion
void f()
{
    . . .
    f();
    . . .
}
void a()
{
    . . .
    b();
    . . .
}
void b()
{
    . . .
    c();
    . . .
}
void c()
{
    . . .
    d();
    . . .
}
void d()
{
    . . .
    a();
    . . .
}
(a)(b)
Direct and indirect recursion.
  1. A function calls itself, forming a short cycle.
  2. A sequence of function calls forms a longer cycle. Branching logic in the functions allows them to call any function in the sequence, forming cycles of various lengths.
Programmers can implement direct recursion without function prototypes because the function definition also serves as the function declaration. Nevertheless, I encourage using prototypes as a good programming style. Alternatively, indirect recursion requires function prototypes because it is impossible to define every function before calling it.

Computer scientists use indirect recursion to solve complex problems such as general expression evaluation or even to implement some kinds of compilers. Nevertheless, this introductory section will focus only on direct or simple recursion.

Recursion Requirements

Recursively calling a general function, one not specifically designed to operate recursively causes runaway recursion - an infinitely long chain of recursive function calls - a logical and ultimately fatal error. In addition to the operations needed to solve a specific problem, three features are necessary for recursion.

  1. There must be one or more paths through the function where recursion occurs.
  2. There must be one or more paths through the function where recursion does not occur. The results for these base cases, which may be implicit for simple functions, must be very easy to calculate (e.g., a constant value).
  3. A value, typically an argument, that changes from one function call to the next. In conjunction with the changing value, a branch statement ends the recursion.

There is a fourth requirement for recursion, but it is a programming language feature and is, therefore, beyond the programmer's control or concern. The programming language must support automatic variables, as the following explains.

How Recursion Works

Two simple, related functions demonstrate recursion. Aside from their names, the function's only difference is the statement order in the if-statement. This seemingly insignificant difference is sufficient to cause the two functions to print the digits of their parameters in different orders.

void forward(int number)
{
    if (number != 0)
    {
	forward(number / 10);
	cout << number % 10;
    }
}
void reverse(int number)
{
    if (number != 0)
    {
	cout << number % 10;
	reverse(number / 10);
    }
}
forward(123) prints 123reverse(123) prints 321
Simple recursion examples. Two functions print the digits of an integer one at a time. Each function satisfies the requirements for recursion: For simplicity, both functions assume that number is non-negative.

When a program calls a function, we say the function is active, and the program creates an activation record - also known as a stack frame - and pushes it on the runtime stack. When the function returns, the program pops the record or frame off the stack. When one function calls another, the first remains active while the second runs - each has a stack frame on the stack. In the case of a recursive function call, the function has multiple activations and multiple stack frames on the runtime stack.

Matching recursive function calls with simplified stack frames is one way to understand recursion. Each frame holds the return address and all the function's local, automatic variables, including its parameters. When the function ends, its frame is popped off and discarded, deallocating the memory for the automatic variables. The following figure illustrates the articulation between the stack frames and the recursive function calls.

A pair of pictures depicting two sets of stack frames, one set each for the forward and reverse functions. Each set has four frames, each corresponding to a function call. Each frame stores one instance of the variable number, whose value changes when the program recursively calls the function. The sequence of values stored in number is 123, 12, 1, and 0. Four instances of the 'forward' function. The client calls the forward function with 123 as the argument, initiating recursion. The first instance calls forward, starting the second instance. Similarly, the second instance calls forward, starting the third instance, which calls forward for the last time, starting the fourth. The fourth instance takes the implied, non-recursive path, ending recursion. The fourth instance returns to the cout statement in the third instance. The third instance returns control to the cout statement in the second, and the second returns control to the cout in the first. Recursion is complete when the first instance ends.
Recursive function calls and stack frames. Each yellow box represents a simplified stack frame pushed on the call stack (2). If a program calls one of the example functions with the argument 123, it creates four stack frames, each storing one instance of the variable number with a different value. The program creates the frames in the order denoted by the numbers between (a) and (b), with the first at the bottom and the last at the top. The numbers beside the statements in (a) and (b) indicate their relative execution order. The initial function calls, labeled 1 in (a) and (b), occur in the client program.
  1. Four calls to forward (statements 1-4) occur before the function produces any output. The program executes the output statements (5-7) after each instance of forward returns.
  2. The reverse function flips the statement order in the if-statement, interleaving the output and recursive function calls.
  3. Recursion can make following the program control (the sequence in which the program executes instructions) challenging. The solid arrows represent function calls, and the numbers in red are the values passed to number in each call. The dashed arrows represent the program control returning from the function when it ends. Although the cout statements follow a forward function call, they don't execute until the forward function ends. The reverse function operates similarly.