problem is solved by reducing it to smaller cases of the same problem To illustrate recursion we shall study some applications and sample pro-
Section 5. 2 • Principles of Recursion
4. Data Structures: Stacks and Trees
We have yet to specify the data structure that will keep track of all these storage areas for functions; to do so, let us look at the tree of function calls. So that an inner function can access variables declared in an outer block, and so that we can return properly to the calling program, we must, at every point in the tree, remember all vertices on the path from the given point back to the root. As we move through the tree, vertices are added to and deleted from one end of this path; the other end (at the root) remains fixed. Hence the vertices on the path form a stack; the storage stacks
areas for functions likewise are to be kept as a stack. This process is illustrated in Figure 5.7.
127 A M Tree of subprogram calls Time Subprogram calls M A B C B B C D E C A A B C B D E M C C M M M M A A B B C C C B B D E E E B C C B Time Stack space
Figure 5.7. A tree of function calls and the associated stack frames
From Figure 5.7 and our discussion, we can immediately conclude that the amount of space needed to implement recursion (which, of course, is related to the number of storage areas in current use) is directly proportional to the height of the time and space
requirements recursion tree. Programmers who have not carefully studied recursion sometimes mistakenly think that the space requirement relates to the total number of vertices in the tree. Thetimerequirement of the program is related to the number of times functions are done, and therefore to the total number of vertices in the tree, but the spacerequirement is only that of the storage areas on the path from a single vertex back to the root. Thus the space requirement is reflected in the height of the tree. A well-balanced, bushy recursion tree signifies a recursive process that can do much work with little need for extra space.
5.2.3 Tail Recursion
Suppose that the very last action of a function is to make a recursive call to itself. In the stack implementation of recursion, as we have seen, the local variables of the
Section 5.2 • Principles of Recursion
175
function will be pushed onto the stack as the recursive call is initiated. When the recursive call terminates, these local variables will be popped from the stack and discarding stack
entries thereby restored to their former values. But doing this step is pointless, because the recursive call was the last action of the function, so that the function now terminates and the just-restored local variables are immediately discarded.
When the very last action of a function is a recursive call to itself, it is thus unnecessary to use the stack, as we have seen, since no local variables need to be preserved. All that we need to do is to set the dummy calling parameters to their new values (as specified for the inner recursive call) and branch to the beginning of the function. We summarize this principle for future reference.
If the last-executed statement of a function is a recursive call to the function itself, 128
then this call can be eliminated by reassigning the calling parameters to the values specified in the recursive call, and then repeating the whole function.
The process of this transformation is shown in Figure 5.8. Part (a) shows the storage areas used by the calling programMand several copies of the recursive function
P, each invoked by the previous one. The colored arrows show the flow of control from one function call to the next and the blocks show the storage areas maintained by the system. Since each call by Pto itself is its last action, there is no need to maintain the storage areas after returning from the call. The reduced storage areas are shown in part (b). Part (c), finally, shows the calls toPas repeated in iterative fashion on the same level of the diagram.
Recursion Tail recursion (a) (b) (c) Iteration M P P P M P P P M P P P
tail recursion This special case when a recursive call is the last-executed statement of the function is especially important because it frequently occurs. It is calledtail re- cursion. You should carefully note that tail recursion means that thelast-executed statement is a recursive call, not necessarily that the recursive call is the last state- ment appearing in the function. Tail recursion may appear, for example, within one clause of aswitchstatement or anifstatement where other program lines appear later.
time and space With most compilers, there will be little difference in executiontimewhether tail recursion is left in a program or is removed. Ifspaceconsiderations are impor- tant, however, then tail recursion should often be removed. By rearranging the termination condition, if needed, it is usually possible to repeat the function using ado whileor awhilestatement.
Consider, for example, a divide-and-conquer algorithm like the Towers of Hanoi. The second recursive call inside functionmoveis tail recursion; the first call is not. By removing the tail recursion, functionmoveof the original recursive program can be expressed as
Hanoi without tail recursion
voidmove(intcount, intstart, intfinish, inttemp)
/*move: iterative version
Pre: Disk count is a valid disk to be moved.
Post: Moves count disks from start to finish using temp for temporary storage.*/ {
intswap; // temporary storage to swap towers while(count>0){ // Replace theifstatement with a loop.
move(count−1,start,temp,finish); // first recursive call
cout << "Move disk" <<count<< "from" <<start
<< "to" <<finish<< "." <<endl;
count−−; // Change parameters to mimic the second recursive call.
swap = start; start = temp; temp = swap; } } 129
We would have been quite clever had we thought of this version of the function when we first looked at the problem, but now that we have discovered it via other considerations, we can give it a natural interpretation. Think of the two towers
startandtempas in the same class: We wish to use them for intermediate storage
as we slowly move all the disks ontofinish. To move a pile of countdisks onto
finish, then, we must move all except the bottom to the other one ofstartandtemp. Then move the bottom one tofinish, and repeat after interchangingstartandtemp, continuing to shuffle all except the bottom one betweenstart andtemp, and, at each pass, getting a new bottom one ontofinish.
5.2.4 When Not to Use Recursion
1. Factorials
Consider the following two functions for calculating factorials. We have already seen the recursive one:
Section 5.2 • Principles of Recursion
177
130
intfactorial(intn)
/*factorial: recursive version
Pre: n is a nonnegative integer.
Post: Return the value of the factorial of n.*/ {
if(n == 0)return1;
else returnn*factorial(n−1); }
There is an almost equally simple iterative version: intfactorial(intn)
/*factorial: iterative version Pre: n is a nonnegative integer.
Post: Return the value of the factorial of n.*/ {
intcount,product = 1;
for(count = 1; count<=n; count++) product*= count; returnproduct; } 1! 2! n! (n – 1)! (n – 2)!
…
0! Figure 5.9.Recursion tree for calculating factorials
Which of these programs uses less storage space? At first glance, it might appear that the recursive one does, since it has no local variables, and the iterative program has two. But actually (see Figure 5.9), the recursive program will set up a stack and fill it with thennumbers
n, n−1, n−2, . . . ,2,1
that are its calling parameters before each recursion and will then, as it works its way out of the recursion, multiply these numbers in the same order as does the second program. The progress of execution for the recursive function applied with n=5 is as follows: factorial(5) = 5*factorial(4) = 5*(4*factorial(3)) = 5*(4*(3*factorial(2))) = 5*(4*(3*(2*factorial(1)))) = 5*(4*(3*(2*(1*factorial(0))))) = 5*(4*(3*(2*(1*1)))) = 5*(4*(3*(2*1))) = 5*(4*(3*6)) = 5*(4*6) = 5*24 = 120.
Thus the recursive program keeps more storage than the iterative version, and it will take more time as well, since it must store and retrieve all the numbers as well as multiply them.