Return to the lecture notes index

Lecture 10 (June 2, 2004)

Recursive Thinking

Today we're going to learn recursion. What happens if you have a method as follows?

public int fn1() { 
   System.out.println("1");
   ...
   y = fn2();
   System.out.println("2");
   ...
}

public int fn2() { 
   System.out.println("3");
   ...
   z = fn3();
   System.out.println("4");
   ...
}
 
public int fn3() {
   System.out.println("5");
   ... 
}
What will the order of the printlns be if we run fn1()? First we print out 1, then call fn2(), which prints out 3 and calls fn3(), which then prints 5, and then returns back to fn2() and goes where it left off there printing 4, and then goes back to fn1() and prints out 2.

A Bit of "Under the Hood"

The compiler makes use of the stack data structure to keep track of method invocation. Think about it: Each time a method is called, execution jumps to a completely different part of the program, and entirely new variables exist. These variables might have the same names as other variables in the program, but they are not the same -- they can have different values. And, once the method returns, these local variables "go away" and execution picks right up where it left off. Additionally, parameters need to be communicated to the method when it called, and a return value needs to be communicated to the caller, upon return.

The compiler does this using a stack, often called the runtime stackreturn address. It then pushes empty space (allocates space) on the stack for all of the local variables. This state, which is stored on the runtime stack, and is assocaited with a single activation of a method, is known as a stack frame.

When the method returns, the compiler pops its stack from from the stack, revealing the return address, and stores the return value on the stack. Upon return, the caller then pops the return value from the stack.

Additionally, there is plenty of other state information associated with the activation fo each function that is stored within the stack frame. For example, if there are only a few parameters, they are often passed in registers, fast memory within the CPU. These registers need to be saved to and restored from the stack when a method is called and upon its return. There are also some pieces of metadata about the stack, itself, such as pointers to the beginning of each stack frame, &c.

For this class, we're not going to get bogged down in the details -- those are covered in 15-213. But, it is critical that you realize that each instance of a function has its own parameters and local variables. It is also critical that you realize that the compiler uses a stack to manage function invocation.

For our purposes, when I draw the runtime stack, I am only going to draw these the things that are critical to us -- the paramters, the local variables, and the return value. And, although the stack is used to communicate the return to the caller, I'm going to, most often, draw in on an arrow connecting one stack from to the next. On the whiteboard, this is just a really clear way of showing that a method returns, popping the stack, and returning a value.

The 2n Example

If asked to compute 2n, you might think about the problem this way: "Start out with one and multiply it by two n times." This is an iterative approach and can be implemented by initializing a value to 1 and multiplying it by 2 n times:


  public static int pow2 (int n)
  {
    int answer=1;

    for (int index=0; index < n; index++)
      answer *=2;

    return answer;
    
  }
  

Another perspective might be this "Multiply 2n-1 by 2. If you don't know 2n-1, figure it out by multiplying 2(n-1)-1 by 2. Nest this logic as deeply as necessary, until you reach -- for example, we know that 20 is 1.

 
  public static int pow2_rec (int n) {
    if (n == 0) {
      // This is the base case -- 20 is 1
      // So if we are asked for 20, we return 1.
      return 1;
    }
    else
      // If we don't know the answer, we use recursive logic to figure 
      // it out by multiplying 2n-1 by 2.
      return (2*pow2_rec(n-1));
  }
  

A Trace of pow2_rec(4)

Let's take a look at the execution of pow2(4).

Methods are invoked using a stack. Each time a method is invoked, the state of the method making the call is pushed into the stack. When a method returns, the state is popped off of the stack and restored. It is important to realize that each instance of a method has its own local variables -- they are actually part of its stack frame.

Our stack works something like as follows when pow2(4) is called:

Somewhere else calls pow2(4)
return    |  parameters  |  local vars 
---------------------------------------
   ?      |       4      |     ---

pow2(4) calls pow2(3)
return    |  parameters  |  local vars 
---------------------------------------
   ?      |       3      |     ---
   ?      |       4      |     ---

pow2(3) calls pow2(2)
return    |  parameters  |  local vars 
---------------------------------------
   ?      |       2      |     ---
   ?      |       3      |     ---
   ?      |       4      |     ---

pow2(2) calls pow2(1)
return    |  parameters  |  local vars 
---------------------------------------
   ?      |       1      |     ---
   ?      |       2      |     ---
   ?      |       3      |     ---
   ?      |       4      |     ---

pow2(1) calls pow2(0).  pow2(0) reaches base case and returns 1.
return    |  parameters  |  local vars 
---------------------------------------
   1      |       0      |     ---
   ?      |       1      |     ---
   ?      |       2      |     ---
   ?      |       3      |     ---
   ?      |       4      |     ---

pow2(1) gets pow2(0)'s return value of 1 and multiplies it by 2, returning its result.
return    |  parameters  |  local vars 
---------------------------------------
   1      |       0      |     ---
   2      |       1      |     ---
   ?      |       2      |     ---
   ?      |       3      |     ---
   ?      |       4      |     ---

pow2(2) gets pow2(1)'s return value of 2 and multiplies it by 2, returning its result.
return    |  parameters  |  local vars 
---------------------------------------
   1      |       0      |     ---
   2      |       1      |     ---
   4      |       2      |     ---
   ?      |       3      |     ---
   ?      |       4      |     ---

pow2(3) gets pow2(2)'s return value of 4 and multiplies it by 2, returning its result.
return    |  parameters  |  local vars 
---------------------------------------
   1      |       0      |     ---
   2      |       1      |     ---
   4      |       2      |     ---
   8      |       3      |     ---
   ?      |       4      |     ---

pow2(4) gets pow2(3)'s return value of 8 and multiplies it by 2, returning its result.
return    |  parameters  |  local vars 
---------------------------------------
   1      |       0      |     ---  
   2      |       1      |     ---  
   4      |       2      |     ---  
   8      |       3      |     ---         
   16     |       4      |     ---

Its result, 16, is then sent back to the place that called pow2(4).

The n! (Factorial) Example

As another example, recursion can be used to calcualte the factorial of a number. Remember that the factorial of a number is defined as follows:
5! = 5 * 4 * 3 * 2 * 1 4! = 4 * 3 * 2 * 1 3! = 3 * 2 * 1 2! = 2 * 1 1! = 1 0! = 1
You can see that 5! = 5 * 4 * 3 * 2 * 1 = 5 * 4! or for any general case n! = n * (n-1)! In recursion, we need a base case or else it will continue on forever. In this case our base case is 0! = 1. Always have the base case first, followed by the general case. We can easily do the factorial method as follows:


  public static int fact(int n)
  {
    if (n == 0) 
      return 1;
    else
      return n*fact(n-1);
  }
  

Notice that like the pow2_rec() method, fact_rec() has a non-recursive case that is guaranteed to occur and will terminate the recursion, allowing it to unwind. If there is no non-recursive case, or if there is not guarantee that the non-recursive case will ever occur, infinite recursion might result. Infinite recursion is much like an endless loop -- the program will run forever (or terminate, because there isn't enough memory left for the stack).

Fibonacci Numbers

The Fibonacci sequence is defined as follows:
Fib0 = 0
Fib1 = 1
Fibn = Fibn-1 + Fibn-2, n > 1

So you'd get something like 0, 1, 1, 2, 3, 5, 8, 13, ...

We can computer the nth number in the Fibonacci sequence readily using recursion:


  public static void fib(int n)
  {
    if (n==0) 
      return 1;
    if (n==1) 
      return 1;

    return (fib(n-1) + fib(n-2));
  }
  

But while this code is very simple, makes sense, and is easy to prove correct, it does have a problem. It is very slow and can't computer large Fibonacci numbers. The problem is that it takes a long time to copy data onto the stack....and the stack will grow very large. Each call to Fib() generates two other calls -- the size of the stack doubles at each level! You can model the growth with a tree, fib(4) is modeled as follows:

                                         --------
                                        | fib(4) |              
                                         --------
                                  /                  \
                               --------            --------
                              | fib(3) |          | fib(2) |
                               --------            --------
                           /           \            /       \
                       --------    --------     --------   --------
                      | fib(2) |  | fib(1) |   | fib(1) | | fib(0) |
                       --------    --------     --------   --------
                    /         \ 
                --------    --------
               | fib(1) |  | fib(0) |
                --------    --------
  

This code can be rewritten nonrecursively to eliminate this limitation and run faster -- but the price is readability:


  public static int fib (int n) {

      int result = 0;
      int nMinusOne = 1;
      int nMinusTwo = 0;

    /*
     * This code works by computing F(1) and then building from
     * it to the number needed. The old computed value is put into
     * nMinusTwo, nMinusOne holds the old result, and result is computed from 
     * nMinusOne and nMinusTwo. Basically, the most recent 2 values are carried from 
     * one iteration to the next, allowing the compution of the next
     * one. This loops until the requested Fibonacci number is computed.
     * It is faster and more scalable than the recursive version -- but
     * much harder to read, understand, or prove correct.
     */
   
      for (int i = 0; i < n; i++) {
        result = nMinusOne + nMinusTwo;
        nMinusTwo = nMinusOne;
        nMinusOne = result;
      }
      return result;
  }
   
The above code calculates the exact same numbers much more efficiently by not having to push and pop methods on the stack multiple times, but is much less elegant and obvious as the recursive definition. This is often a tradeoff with recursion. Often you'll have problems that can easily be coded with recursion, but are very inefficient when ran. In most cases, you can code it without recursion, but it might be more difficult and time-consuming to code. In this way, recursion is often a double-edged sword, and you need to learn when to use it and when not to.