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:
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) * nWe 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:
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:
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 the 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:
This section describes some of the ways in which recursive functions are characterized. The characterizations are based on:
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.
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); }
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.
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 6,
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:
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:
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);
Thomas A. Anastasio, Thu Sep 4 21:40:50 EDT 1997
Modified by Richard Chang Fri Jan 16 22:51:58 EST 1998.