This lets you do something very important. It lets you make a call to the same procedure that you are currently in. This runs a second copy of the procedure, which will restore the first copy when it returns.
Let's take a simple example (in rec1.c):
/* 1 */ a(int i) /* 2 */ { /* 3 */ printf("In procedure a: i = %d\n", i); /* 4 */ if (i == 10) a(9); /* 5 */ } /* 6 */ /* 7 */ main() /* 8 */ { /* 9 */ a(10); /* 10 */ }You'll note, if i equals 10, then a() calls itself. Let's look at what happens when this is executed. First, we are in main(), and it calls a(10). What happens here is that the computer stores its current context (where it is, and what its local variables are) on the stack. The stack looks like:
top --> | [main(): line 9] |
Then a(10) is executed. It will print:
In procedure a: i = 10and then it will call a(9). Once again, the computer stores its current context on the stack. The stack now looks like:
top --> | [a(): line 4, i = 10] |
[main(): line 9] |
Then a(9) is executed. It will print:
In procedure a: i = 9and then it will return. When it returns, it pops where it should return off the stack -- this is in procedure a() at line 4, with i equal to 10. The stack once again looks like:
top --> | [main(): line 9] |
Now, the first thing that happens is that a(10) returns. Again, it pops where it should return off the stack -- this is in procedure main() at line 9. Of course, what happens is that main() exits, and the program ends. Thus, the output is:
In procedure a: i = 10 In procedure a: i = 9
/* 1 */ a(int i) /* 2 */ { /* 3 */ int j; /* 4 */ /* 5 */ j = i*5; /* 6 */ printf("In procedure a: i = %d, j = %d\n", i, j); /* 7 */ if (i > 0) a(i-1); /* 8 */ printf("Later In procedure a: i = %d, j = %d\n", i, j); /* 9 */ } /* 10 */ /* 11 */ main() /* 12 */ { /* 13 */ int i; /* 14 */ /* 15 */ i = 16; /* 16 */ a(3); /* 17 */ printf("main: %d\n", i); /* 18 */ }Again, let's see what happens when it is executed. First, we're in main() which sets i to 16 and calls a(3). This pushes the current context on the stack:
top --> | [main(): line 16, i = 16] |
Now, we execute a(3). This sets j to 15, and prints out:
In procedure a: i = 3, j = 15It then calls a(2). This pushes the current context on the stack:
top --> | [a(): line 7, i = 3, j = 15] |
[main(): line 16, i = 16] |
And then we call a(2). This sets j to 10, and prints out:
In procedure a: i = 2, j = 10And then it calls a(1). Once again, the current context is pushed onto the stack:
top --> | [a(): line 7, i = 2, j = 10] |
[a(): line 7, i = 3, j = 15] | |
[main(): line 16, i = 16] |
And then we execute a(1). This sets j to 5, and prints out:
In procedure a: i = 1, j = 5And then it calls a(0). Once again, the current context is pushed onto the stack:
top --> | [a(): line 7, i = 1, j = 5] |
[a(): line 7, i = 2, j = 10] | |
[a(): line 7, i = 3, j = 15] | |
[main(): line 16, i = 16] |
And then we execute a(0). This sets j to 0, and prints out:
In procedure a: i = 0, j = 0Since i is zero, it skips the body of the if statement, prints out:
Later In procedure a: i = 0, j = 0and returns. Now what returning does is restore the top context on the stack, which means that we are in a() at line 7 with i = 1 and j = 5. The stack is now:
top --> | [a(): line 7, i = 2, j = 10] |
[a(): line 7, i = 3, j = 15] | |
[main(): line 16, i = 16] |
It prints out:
Later In procedure a: i = 1, j = 5and a(1) returns. Once again, we restore the top context on the stack, which means that we are in a() at line 7 with i = 2 and j = 10. The stack is now:
top --> | [a(): line 7, i = 3, j = 15] |
[main(): line 16, i = 16] |
It prints out:
Later In procedure a: i = 2, j = 10and a(2) returns. Once again, we restore the top context on the stack, which means that we are in a() at line 7 with i = 3 and j = 15. The stack is now:
top --> | [main(): line 16, i = 16] |
It prints out:
Later In procedure a: i = 3, j = 15and a(3) returns. Finally, we restore the last context on the stack, which means that we are in main() at line 16 with i = 16. The stack is now empty. It prints out:
main: 16and exits. Thus, the whole output is:
In procedure a: i = 3, j = 15 In procedure a: i = 2, j = 10 In procedure a: i = 1, j = 5 In procedure a: i = 0, j = 0 Later In procedure a: i = 0, j = 0 Later In procedure a: i = 1, j = 5 Later In procedure a: i = 2, j = 10 Later In procedure a: i = 3, j = 15 main: 16
a(int i) { printf("In procedure a: i = %d\n", i); a(i); } main() { a(10); }When you run it, it looks like an infinite loop:
UNIX> rec3 In procedure a: i = 10 In procedure a: i = 10 In procedure a: i = 10 In procedure a: i = 10 ....One difference between infinite recursion and most infinite loops is that you will run out of stack space eventually with infinite recursion and the program will exit. On my machine, if you remove the print statement from rec3.c and run it, it eventually seg faults.
int factorial(int i) { int f; f = 1; while (i > 0) { f *= i; i--; } }However, you can also do it recursively. Remember the definition of factorial:
int factorial(int n) { int f; if (n <= 0) return 1; return n * factorial(n-1); }Go ahead and run fact1 and fact2 and see that they return the same output. Use gdb to look at the state of fact2 if you're still a little leery of recursion.
int imult(int a, int b) { int product; product = 0; while (b > 0) { product += a; b--; } return product; }Try it:
UNIX> mult1 4 10 40 UNIX> mult1 10 4 40 UNIX>Or you could do that recursively (in mult2.c):
int imult(int a, int b) { int product; if (b <= 0) return 0; return a + imult(a, b-1); }They both work, but mult1 runs faster because it doesn't have to do those stack operations like mult2 has to. Unfortunately, this is hard to time because if you try to use large values of b, mult2 will run out of stack space and seg fault. One way to time it is to run both a lot of times. If you look at mult1.sh and mult2.sh, these are shell scripts that run mult1 and mult2 20 times. If you time them, you'll see that mult1 is much faster:
UNIX> time sh mult1.sh 1000000 ... 1000000 0.0u 0.1s 0:00.33 54.5% 128+225k 0+0io 0pf+0w UNIX> time sh mult2.sh 1000000 ... 1000000 0.7u 0.4s 0:01.36 86.0% 28+1852k 0+0io 0pf+0w UNIX>What this means is that:
int fibonacci(int n) { if (n <= 1) return 1; return fibonacci(n-1) + fibonacci(n-2); }It works fine for small values of n:
UNIX> fib1 1 1 UNIX> fib1 2 2 UNIX> fib1 3 3 UNIX> fib1 4 5 UNIX> fib1 5 8 UNIX> fib1 6 13 UNIX>However, if you think about it, the running time of this program is brutal. Suppose we only care about the number of times that fibonacci() is called. Let this be F(n). F(0) = 1 and F(1) = 1. F(n) is 1+F(n-1)+F(n-2), which means that F(n) is greater than F(n-1)+F(n-2). So, F(2) > 2, F(3) > 3, F(4) > 5, etc. You'll see F(n) is greater than fib(n). As the book will tell you, fib(n) = O(3/5)^n, which means that the running time of fib1.c is exponential, which is terrible. You'll notice that when you get to values of n in the 30's, fib1 starts slowing down incredibly.
Of course, it doesn't have to be that way. A simple while loop does it in O(n) time. This is in fib2.c, and this one can do fib(40), for example, blazingly fast.
int fibonacci(int n) { int fibim1, fibim2, fibi, i; if (n <= 1) return 1; fibim1 = 1; fibim2 = 1; i = 1; while (1) { i++; fibi = fibim1 + fibim2; if (i == n) return fibi; fibim2 = fibim1; fibim1 = fibi; } }Note, that the book discusses recursion in section 1.3, and fibonacci numbers in section 2.4.2.
There is a very elegant solution to this. If n is one, then you simply move the disk. Otherwise, you solve the problem for n-1, moving the top n-1 disks to tower 2, and then you move the bottom disk to tower 1. Finally, you use the solution for n-1 to move the n-1 disks from tower 2 to tower 1.
This maps very well into a recursive subroutine. But before we do that, we need to actually code up the towers of Hanoi.
In towers.c, I implement three procedures. The first is:
Dllist *new_towers(int n);which creates and returns the array of three dllists. Tower 0 will have n disks on it. This is, of course, straightforward.
Dllist *new_towers(int npiece) { Dllist *t; int i; int piece; /* Allocate the array */ t = (Dllist *) malloc(sizeof(Dllist)*3); /* Create the dllists */ for (i = 0; i < 3; i++) t[i] = new_dllist(); /* Put the disks onto tower 0 */ for (piece = 1; piece <= npiece; piece++) { dll_append(t[0], new_jval_i(piece)); } /* Return the towers */ return t; }Next, we write make_move() which moves the top piece from one tower to another. This one has to do error checking to make sure that the source tower is not empty, and that the destination tower does not have a disk on it that is too small. The code below does this. It also prints out the move:
make_move(Dllist *towers, int from, int to) { int piece, topofto; /* Error check -- is the first tower empty? */ if (dll_empty(towers[from])) { printf("Illegal move of tower %d to %d\n", from, to); return; } piece = jval_i(dll_val(dll_first(towers[from]))); /* Error check -- is the piece too big to go on the destination tower? */ if (!dll_empty(towers[to])) { topofto = jval_i(dll_val(dll_first(towers[to]))); if (piece > topofto) { printf("Illegal move of tower %d to %d\n", from, to); return; } } /* Move the piece from the first tower to the second */ dll_delete_node(dll_first(towers[from])); dll_prepend(towers[to], new_jval_i(piece)); /* Print that the piece has moved */ printf("Moved piece %3d from tower %d to tower %d\n", piece, from, to); }Finally, we write print_towers() which prints out the towers. This one is really simple:
print_towers(Dllist *towers) { int i; Dllist tmp; for (i = 0; i < 3; i++) { printf("Tower %d:", i); dll_rtraverse(tmp, towers[i]) { printf("%3d", jval_i(dll_val(tmp))); } printf("\n"); } }One of the points of all this is that you should be able to take a problem description like the Towers of Hanoi, and code it up. If you have your data structures right (like here, using an array of three dllists), writing the code is simple.
solve_tower(Dllist *towers, int from, int to, int npieces) { int other; int i; /* Find the identity of the other tower */ for (i = 0; i < 3; i++) { if (from != i && to != i) other = i; } /* Move the top n-1 pieces to the other tower */ if (npieces > 1) { solve_tower(towers, from, other, npieces-1); } /* Move the remaining piece to the destination */ make_move(towers, from, to); /* Move the top n-1 pieces onto the destination */ if (npieces > 1) { solve_tower(towers, other, to, npieces-1); } }Try it out. Can you figure out the number of calls to make_move() as a function of n?
UNIX> tower_solution 1 Tower 0: 1 Tower 1: Tower 2: Moved piece 1 from tower 0 to tower 1 Tower 0: Tower 1: 1 Tower 2: UNIX> tower_solution 2 Tower 0: 2 1 Tower 1: Tower 2: Moved piece 1 from tower 0 to tower 2 Moved piece 2 from tower 0 to tower 1 Moved piece 1 from tower 2 to tower 1 Tower 0: Tower 1: 2 1 Tower 2: UNIX> tower_solution 3 Tower 0: 3 2 1 Tower 1: Tower 2: Moved piece 1 from tower 0 to tower 1 Moved piece 2 from tower 0 to tower 2 Moved piece 1 from tower 1 to tower 2 Moved piece 3 from tower 0 to tower 1 Moved piece 1 from tower 2 to tower 0 Moved piece 2 from tower 2 to tower 1 Moved piece 1 from tower 0 to tower 1 Tower 0: Tower 1: 3 2 1 Tower 2: UNIX>