CMSC--202 Computer Science II for Majors
Fall 1997 4 September 1997 Notes on Recursion
Recursion is a technique that allows us to break down a problem into one or
more subproblems that are similar in form to the original problem. For
example, suppose we need to add up all of the numbers in an array. We'll
write a function called add_array
that takes as arguments an array of
numbers and a count of how many of the numbers in the array we would like to
add; it will return the sum of that many numbers.
If we had a function that would add up all but the very last number in the
array, then we would simply have to add the last number to that sum and we
would be done. Add_array
is an ideal function for adding up all but
the last number (as long as the array contains at least one number). After
all, add_array
is responsible for taking an array and a count, and
adding up that many array elements. If there are no numbers in the array,
then zero is the desired answer. These observations suggest the following
function:
int add_array(int arr[], int count) { if (count == 0) return 0; return arr[count - 1] + add_array(arr, count - 1); }This function is perfectly legal C, and it operates correctly. Notice that the function has two components:
if
and the
return 0
, in which the function does not call
itself. This handles the case where there are no numbers to add.
add_array
is used to add together count-1
items; the count
-th item is then added to this result (remember
that the n
-th item of an array is stored at position n-1
).
add_array
from inside add_array
is called a
recursive call.
One of the classic examples of recursion is the factorial function. Although factorial is not the world's most interesting function, it will provide us with many useful observations.
Recall that factorial, which is written n!
, has the following
definition:
n! = 1 * 2 * 3 * .... * (n-2) * (n-1) * n
We can use this definition to write a C function that implements factorial:
int fact(int n) { int i; int result; result = 1; for (i = 1; i <= n; i++) result = result * i; return result; }This is a simple iterative function that mirrors the definition of factorial. We can derive a different definition for factorial by noticing that
n! = n * (n-1)!
and 1! = 1
.
For example, 4! = 4 * 3!
.
Notice that we need to specify a value for 1!
because our
definition does not apply when n=1
.
This kind of definition is known as an inductive
definition, because it defines a function in terms of itself.
We can write a C function that mirrors this new definition of factorial as follows:
int fact(int n) { if (n == 1) return 1; return n * fact(n - 1); }Notice that this function precisely follows our new definition of factorial. It is recursive, because it contains a call to itself.
Let's compare the two versions:
fact
function! It does so by making the
computer do more work, so that you can do less work.
To successfully apply recursion to a problem, you must be able to
break the problem down into subparts, at least one of which is similar
in form to the original problem. For example, suppose we want to
count the number of occurrences of the number 42 in an array of n
integers. The first thing we should do is write the header for our
function; this will ensure that we know what the function is supposed
to do and how it is called:
int count_42s(int array[], int n);To use recursion on this problem, we must find a way to break the problem down into subproblems, at least one of which is similar in form to the original problem. If we know that the array contains
n
numbers, we might break our task into the subproblems of:
n-1
elements of the array (this is a subproblem that is similar in
form to the original problem);
n
-th element of the array ( i.e. determining whether the
n
-th element is 42); and
count_42s(array, n-1);
If successful, this recursive call will count all of the occurrences
of the number 42 in the first n-1
positions of the array and
return the sum (we will discuss the conditions that must hold for a
recursive call to be successful in Section 5; until
then, we will assume that all recursive calls work properly). We must
now determine how to use this result. If the last element in the
array is not 42, then the number of 42s in the entire array is the
same as the number of 42s in all but the last element of the array.
If the last number in the array is 42, then the number of 42s in the
entire array is one more than the number found in the subarray. This
suggests the following code:
if (array[n-1] != 42) return count_42s(array, n-1); return 1 + count_42s(array, n-1);Here we have two recursive calls (only one of which will actually be used in any given situation). We must now determine whether there are any circumstances under which this code will not work. In fact, this code will not work when
n==0
; in such a case it tries to
subscript the array with -1, which is not a legal array subscript in
C. (Oh alright, it's legal; it's just that it's almost never
what you want, and will often lead to a segmentation fault or worse.)
That means that unless we treat specially the case where n
is
zero, our function will not work when asked to count the number
of 42s in an array of zero items. We will therefore add a base case
that will test for n==0
and return zero as its result in that
case. This gives the function:
int count_42s(int array[], int n) { if (n==0) return 0; if (array[n-1] != 42) return count_42s(array, n-1); return 1 + count_42s(array, n-1); }This is a perfectly good recursive solution to the
count_42s
problem. It is not the only recursive solution though; there are
other ways to break the problem into subpieces. For example, we could
break the array into two pieces of equal size, count the number of 42s
in each half, then add the two sums. To do this, we will need to hand
as arguments to count_42s
not just the array and the subscript
of the highest value in the array, but also the subscript of the
lowest value in the array:
int count_42s(int array[], int low, int high);A call such as
count_42s(my_array, A, B)
says ``count all the
occurrences of the number 42 in my_array
that lie between
position A
and position B
inclusive.''
We can calculate the midpoint between subscript low
and
subscript high
with (high+low)/2
. Thus we can count
the number of 42s in each half of the array and add them together
with:
count_42s(array, low, (low + high) / 2) + count_42s(array, (low + high) / 2 + 1, high);We now have a recursive case but no base case. When will the recursive case fail? It fails when the array does not contain at least two numbers. If the array contains no items, or it contains one item that is not 42, then we should return zero. If the array contains exactly one number, and that number is 42, then we should return one. Putting it all together, we get:
int count_42s(int array[], int low, int high) { if ((low > high) || (low == high && array[low] != 42)) return 0; if (low == high && array[low] == 42) return 1; return count_42s(array, low, (low + high)/2) + count_42s(array, 1 + (low + high)/2, high)); }Note that the line
if (low == high && array[low] == 42)
could properly be
written simply as if (low==high)
. The comparison with 42 is
included here simply to make each of the relevant conditions explicit
in the same expression.
These examples demonstrate that there may be many ways to break a problem down into subproblems such that recursion is useful. It is up to you the programmer to determine which decomposition is best. The general approach to writing a recursive program is to:
How should you think about a recursive subprogram? Do not
immediately try to
trace through the execution of the recursive calls; doing so is likely to
simply confuse you.
Rather, think of recursion as working via the power of
wishful thinking. Consider the operation of fact(4)
using the recursive formulation of fact. 4 is not 1, so
the recursive case holds. The recursive case says to multiply
fact(3) by 4. Here is where the wishful
thinking comes in: wish for fact(3) to be
calculated. Because this is a recursive call, your wish will be
granted. You now know that fact(3)=6. So
fact(4) is equal to 6 times
4, or 24 (which is just what it's supposed to
be).
An analogy you can use to help you think this way is corporate management. When the CEO of a corporation tells a vice-president to perform some task, the CEO doesn't worry about how the task is accomplished; he or she relies on the vice-president to get it done. You should think the same way when you are programming recursively. Delegate the subtask to the recursive call; don't worry about how the task actually gets done. Worry instead whether the top-level task will get done properly, given that all the recursive calls work properly.
Another way to think about recursion is to pretend that a recursive call is actually a call to a different function, written by somebody else, that performs the same task that your function performs. For example, suppose we had a library routine called libfact that returned the factorial of its argument. We could then write our own version of fact as:
int fact (int n) { if (n == 1) return 1; return n * libfact(n - 1); }This version of fact correctly returns one if its argument is one. If its argument is greater than one, it calls libfact to calculate (n-1)!, and multiplies the result by n. Because libfact is a library routine, we may assume that it works properly, in this case calculating the factorial of n-1. For example, if n is 4, then libfact is called with 3 as its argument; it returns 6. This is multiplied by 4 to get the desired result of 24.
This example points out that a recursive call is just like any other function call. In particular, a recursive call gets its own parameter list and local variables, just as libfact would. Furthermore, while the recursive call is executing, the top--level call sits there waiting for the recursive call to terminate. This means that execution doesn't halt when a recursive call finds itself at the base case; once the recursive call returns, the top--level call then continues to execute.
One of the most difficult aspects of programming recursively is the mental process of accepting on faith that the recursive call will do the right thing. The following checklist itemizes the five conditions that must hold for recursion to work. If each of these conditions holds for your recursive subprogram, you may feel confident that the recursion will operate correctly:
Let's see whether the recursive fact function meets these criteria:
if (n==1) return 1
is
a base case, while the ``else
'' part includes a recursive
call (fact(n-1)
).
n
is one then n!
is one. Our function
correctly returns 1 when n is 1. If n
is not one,
the definition says that we should return (n-1)! * n
. The
recursive call (which we now assume to work properly) returns the value
of n-1 factorial, which is then multiplied by n. Thus,
the non--recursive portions of the function behave as required.
This section describes some of the ways in which recursive functions are characterized. The characterizations are based on:
A C function is directly recursive if it contains an
explicit call to itself.
For example, the function
int foo(int x) { if (x <= 0) return x; return foo(x - 1); }includes a call to itself, so it's directly recursive. The recursive call will occur for positive values of
x
.
A C function
foo
is indirectly
recursive if it contains a call to another function which
ultimately calls foo
.
The following pair of functions is indirectly recursive. Since they call each other, they are also known as mutually recursive functions.
int foo(int x) { if (x <= 0) return x; return bar(x); } int bar(int y) { return foo(y - 1); }
A recursive function is said to be tail recursive if
there are no pending operations to be performed on return from a
recursive call.
Tail recursive functions are often said to ``return the
value of the last recursive call as the value of the function.''
Tail recursion is very desirable because the amount of information
which must be stored during the computation is independent of the
number of recursive calls. Some modern computing systems will actually
compute tail-recursive functions using an iterative process.
The ``infamous'' factorial function fact
is usually written in
a non-tail-recursive manner:
int fact (int n) /* n >= 0 */ { if (n == 0) return 1; return n * fact(n - 1); }Notice that there is a ``pending operation,'' namely multiplication, to be performed on return from each recursive call. Whenever there is a pending operation, the function is non-tail-recursive. Information about each pending operation must be stored, so the amount of information is not independent of the number of calls.
The factorial function can be written in a tail-recursive way:
int fact_aux(int n, int result) { if (n == 1) return result; return fact_aux(n - 1, n * result) } int fact(n) { return fact_aux(n, 1); }The ``auxiliary'' function
fact_aux
is used to keep the syntax
of fact(n)
the same as before. The recursive function is
really fact_aux
, not fact
. Note that fact_aux
has no pending operations on return from recursive calls. The value
computed by the recursive call is simply returned with no
modification. The amount of information which must be stored is
constant (the value of n
and the value of result
),
independent of the number of recursive calls.
Another way to characterize recursive functions is by the way in which the recursion grows. The two basic ways are ``linear'' and ``tree.''
A recursive function is said to be linearly
recursive when no pending operation involves another recursive call to
the function.
For example, the ``infamous'' fact
function is linearly
recursive. The pending operation is simply multiplication by a
scalar, it does not involve another call to fact
.
A recursive function is said to be tree recursive (or
non-linearly recursive) when the pending operation does involve
another recursive call to the function.
The Fibonacci function fib
provides a classic example of tree
recursion. The Fibonacci numbers can be defined by the rule:
fib(n) = 0 if n is 0, = 1 if n is 1, = fib(n-1) + fib(n-2) otherwiseFor example, the first seven Fibonacci numbers are
Fib(0) = 0 Fib(1) = 1 Fib(2) = Fib(1) + Fib(0) = 1 Fib(3) = Fib(2) + Fib(1) = 2 Fib(4) = Fib(3) + Fib(2) = 3 Fib(5) = Fib(4) + Fib(3) = 5 Fib(6) = Fib(5) + Fib(4) = 8This leads to the following implementation in C:
int fib(int n) /* n >= 0 */ { if (n == 0) return 0; if (n == 1) return 1; return fib(n - 1) + fib(n - 2); }Notice that the pending operation for the recursive call is another call to
fib
. Therefore fib
is tree-recursive.
A non-tail recursive function can often be converted to a tail-recursive function by means of an ``auxiliary'' parameter. This parameter is used to form the result. The idea is to attempt to incorporate the pending operation into the auxiliary parameter in such a way that the recursive call no longer has a pending operation. The technique is usually used in conjunction with an ``auxiliary'' function. This is simply to keep the syntax clean and to hide the fact that auxiliary parameters are needed.
For example, a tail-recursive Fibonacci function can be implemented by
using two auxiliary parameters for accumulating results.
It should not be surprising that the
tree-recursive fib
function requires two auxiliary parameters
to collect results; there are two recursive calls. To compute
fib(n)
, call fib_aux(n 1 0)
int fib_aux(int n, int next, int result) { if (n == 0) return result; return fib_aux(n - 1, next + result, next); }
Let's assume that tail recursive functions can be expressed in the general form
F(x) { if (P(x)) return G(x); return F(H(x)); }That is, we establish a base case based on the truth value of the function
P(x)
of the parameter. Given that P(x)
is
true, the value of F(x)
is the value of some other function
G(x)
. Otherwise, the value of F(x)
is the value of the
function F
on some other value, H(x)
.
Given this formulation, we can immediately write an iterative version
as
F(x) { int temp_x = x; while (P(x) is not true) { temp_x = x; x = H(temp_x); } return G(x); }The reason for using the local variable
temp_x
will become
clear soon. Actually, we will use one temporary variable for each
parameter in the recursive function.
In the tail-recursive factorial function (fact_aux
) given in
Section 7,
F
is fact_aux
x
is composed of the two parameters, n
and
result
P(n, result)
is the value of (n == 1)
G(n, result)
is result
H(n, result)
is (n -1, n * result)
Therefore the iterative version is:
int fact_iter(int n, int result) { int temp_n; int temp_result; while (n != 1) { temp_n = n; temp_result = result; n = temp_n - 1; result = temp_n * temp_result; } return result; }The variable
temp_n
is needed so result
will be computed
on the basis of the unchanged n
. The variable
temp_result
is not really needed, but is used to be
consistent.
In the tail-recursive fibonacci function (fib_aux
) given in
Section 7,
fib_aux
x
is composed of the three parameters n
,
next
, and result
P(n, next, result)
is the value of
(n == 0)
G(n, next, result)
is result
H(n, next, result)
is
(n -1, next + result, next)
Therefore the iterative version is
int fib_iter(int n, int next, int result) { int temp_n; int temp_next; int temp_result; while (n != 0) { temp_n = n; temp_next = next; temp_result = result; n = temp_n - 1; next = temp_next + temp_result; result = temp_next; } return result; }
Just as it is possible to convert any recursive function to iteration, it is possible to convert any iterative loop into the combination of a recursive function and a call to that function. The ability to convert recursion to iteration is often quite useful, allowing the power of recursive definition with the (often) greater efficiency of iteration.
Converting iteration to recursion is unlikely to be useful in practice, but it is a fine learning tool. This Section gives several examples of the relationship between programs that loop using C's built-in iteration constructs, and programs that loop using tail recursion.
Suppose that we want to write a program that will read in a sequence of numbers, and print out both the maximum value and the position of the maximum value in the sequence. For example, if the input to our program is the sequence 2, 5, 6, 4, 1, then the program should tell us that the maximum number is 6, and that it occurs in position 3 of the input. Here is a program that performs this task using C's built-in iterative constructs:
#include <stdio.h> #include <assert.h> #include <stdlib.h> #define MAX_NUMS 32 #define max(num1,num2) ((num1) > (num2) ? (num1) : (num2)) typedef int array_of_nums[MAX_NUMS]; int main(void) { array_of_nums nums; int num_count; int max_num; int pos_of_max; int i; /* LOOP 1: Read in the numbers */ num_count = 0; while(scanf("%d", &nums[num_count]) != EOF) num_count++; assert(num_count > 0); /* LOOP 2: Find the maximum number */ max_num = nums[0]; for (i = 1; i < num_count; i++) max_num = max(max_num, nums[i]); /* LOOP 3: Find the position of the maximum number */ pos_of_max = 0; while (nums[pos_of_max] != max_num) pos_of_max++; /* Print the results */ printf("The largest number, which was %d, occurred in position number %d\n", max_num, pos_of_max + 1); return EXIT_SUCCESS; }
You will notice that the above program uses three loops and an array where one loop and an integer would have sufficed. However, the purpose of this program is not to show the best way to find the maximum of a sequence of numbers, but rather to exhibit a program that contains a few loops.
We will convert each of the program's three loops into a tail recursive subprogram. The key to implementing a loop using tail recursion is to determine which variables capture the state of the computation; these variables will then serve as the parameters to the tail recursive subprogram. The first loop is a while loop that is responsible for reading numbers into an array. It terminates when scanf returns EOF. Aside from the EOF condition, two variables capture the state of the computation each time through this loop:
To translate this loop into a tail recursive subprogram then, we will write a subprogram that takes these two values as arguments. Since we are interested in getting a final value for num_count, we will write a function that returns num_count as its result. If the termination condition ( EOF) is not true, the procedure will increment num_count, read a number into nums, and call itself recursively to input the remainder of the numbers. If the termination condition is true, the procedure will simply return the final value of num_count. Here is the code:
int read_nums(int num_count, array_of_nums nums) { if (scanf("%d", &nums[num_count]) != EOF) return read_nums(num_count + 1, nums); return num_count; }
Notice that the recursive call to read_nums is closer to the base case ( EOF) than the top--level call by virtue of the fact that the call to scanf consumes input. The base case test happens before the recursive call, and the base case will never be skipped. Finally, if there is no more input, we return the current value of num_count, which has accumulated exactly the return value we desire. If there is more input, we add one to num_count and recurse. In either case, if we assume that the recursive call works properly, we get exactly the return value we want. Thus this function meets each of our criteria for valid recursive functions.
We must invoke this procedure with the correct initial values for its arguments; here is the appropriate code from the main program:
/* Read in the numbers */ num_count = read_nums(0, nums); assert(num_count > 0);
Notice that this call to read_nums causes the initial value of
num_count in read_nums to be zero (which is exactly the
initial value it had in the original loop).
Assert is a statement defined in <assert.h>
; it flags
an error if its argument is false, and does nothing if its argument is
true. The assert statement is used to ensure that at least one
number is read in. Without this statement, subsequent code will bomb
if no numbers are entered.
Let's generalize from this example. In general, an iterative loop may be converted to a tail--recursive function by:
The second loop is a for loop that is responsible for finding the value of the maximum input number. Before entering the for loop, we make the assumption that the zero-th number read in is the largest. We then loop through all but the zero-th element of nums, looking for a higher value. Four variables capture the state of the computation during this loop:
int find_max(int max_so_far, int pos_to_test, int num_count, array_of_nums nums) { if (pos_to_test < num_count) return find_max(max(max_so_far, nums[pos_to_test]), pos_to_test + 1, num_count, nums); return max_so_far; }
To invoke this function, we will need to pass the value of the zero-th number read in as its first argument, and 1 (which was the initial value of the loop control variable in the iterative version of the program) as its second argument. Here is the appropriate portion of the main program:
/* find the maximum number */ max_num = find_max(nums[0], i, num_count, nums);
The third loop looks through the input numbers until it finds the first occurrence of the maximum number, then records the position of the maximum number. Three variables control the state of this computation:
int find_pos_of_max(int max_num, int pos_to_test, array_of_nums nums) { if (nums[pos_to_test] != max_num) return find_pos_of_max(max_num, pos_to_test + 1, nums); return pos_to_test; }
Note that although the recursive call is not textually the last code of the function, it is the last instruction executed before the return if we have not yet found the position of the maximum number. Therefore, it is a tail recursive call. The invocation of this function in the main program looks as follows:
/* Find the position of the maximum number */ pos_of_max = find_pos_of_max(max_num, 0, nums);