CS202 Lecture notes -- Recursion

  • James S. Plank
  • Directory: ~jplank/cs202/Notes/Recursion
  • Lecture notes: http://web.eecs.utk.edu/~jplank/plank/classes/cs202/Notes/Recursion/index.html
  • Original Lecture Notes: I'm guessing the mid 2000's.
  • Last modification: Mon Oct 28 16:41:06 EDT 2019
    Here is some practice material with recursion:

    Recursion

    Recursion is an extremely important programming technique -- one that students seem to have trouble with early. It's a very simple concept. If a language supports recursion (and most of them do, Fortran being a notable exception), then whenever you make a procedure call, the computer stores a few things, which I call a context: It actually stores these things by pushing them onto a stack (which we will heretofore call "the stack""). Thus, whenever a procedure call returns, it knows what to do by popping the context off the stack. That's how it "resumes" from the procedure call.

    This lets you do something very important. It lets you make a call to the same procedure that you are currently running. This runs a second copy of the procedure, which will restore the first copy when it returns.

    Let's take a simple example (in src/rec1.cpp):

    /* 1 */     void a(int i)
    /* 2 */     {
    /* 3 */       printf("In procedure a: i = %d\n", i);
    /* 4 */       if (i == 10) a(9);
    /* 5 */     }
    /* 6 */
    /* 7 */     int main()
    /* 8 */     {
    /* 9 */       a(10);
    /* 10 */      return 0;
    /* 11 */    }
    

    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 = 10
    
    and 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 = 9
    
    At this point, a(9) returns. 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]

    After being returned to, 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() returns, and the program ends. Thus, the output is:

    In procedure a: i = 10
    In procedure a: i = 9 
    

    A slightly more complex example

    Now, look at src/rec2.cpp:

    /* Line  1 */   void a(int i)
    /* Line  2 */   {
    /* Line  3 */     int j;
    /* Line  4 */   
    /* Line  5 */     j = i*5;
    /* Line  6 */     printf("In procedure a: i = %d, j = %d\n", i, j);
    /* Line  7 */     if (i > 0) a(i-1);
    /* Line  8 */     printf("Later In procedure a: i = %d, j = %d\n", i, j);
    /* Line  9 */   }
    /* Line 10 */   
    /* Line 11 */   int main()
    /* Line 12 */   {
    /* Line 13 */     int i;
    /* Line 14 */     
    /* Line 15 */     i = 16;
    /* Line 16 */     a(3);
    /* Line 17 */     printf("main: %d\n", i);
    /* Line 18 */     return 0;
    /* Line 19 */   }
    

    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 = 15
    
    It 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 = 10
    
    And 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 = 5
    
    And 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 = 0 
    
    Since i is zero, it skips the body of the if statement, prints out:
    Later In procedure a: i = 0, j = 0
    
    and 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 = 5
    
    and 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 = 10
    
    and 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 = 15
    
    and 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: 16
    
    and exits. Thus, the whole output is:
    UNIX> bin/rec2
    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
    UNIX> 
    

    Using gdb to look at the stack

    See this web page for an example of using gdb to look at the stack while rec2.cpp is running.

    Infinite recursion

    Obviously, just like you can write a program that goes into an infinite for() loop, you can write one that goes into an infinite recursive loop, like src/rec3.cpp:

    /* This shows infinite recursion.  Unlike an infinite loop, which will
       run forever until interrupted, infinite recursion will eventually
       run out of stack space.  The result is usually a segmentation 
       violation, but sometimes it is something else like an illegal 
       instruction */
    
    #include <cstdio>
    using namespace std;
    
    void a(int i)
    {
      printf("In procedure a: i = %d\n", i);
      a(i);
    }
    
    int 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 macintosh (in 2019), it eventually exits with a segmentation violation.

    Standard recursion examples - factorial

    One standard recursion example is computing a factorial of a number. This can be done with a simple while loop as in src/fact1.cpp

    /* This is a simple example of computing a factorial with a while loop. 
       This program reads numbers on standard input, and prints their factorials. */
    
    #include <iostream>
    using namespace std;
    
    long long factorial(long long i)
    {
      long long f;
    
      f = 1;
      while (i > 0) {
        f *= i;
        i--;
      }
      return f;
    }
    
    int main()
    {
      long long i;
    
      while (cin >> i) cout << "Factorial of " << i << " is " << factorial(i) << endl;
      return 0;
    }
    

    We can verify that it works:

    UNIX> echo 2 3 4 5 6 7 8 9 10 | bin/fact1
    Factorial of 2 is 2
    Factorial of 3 is 6
    Factorial of 4 is 24
    Factorial of 5 is 120
    Factorial of 6 is 720
    Factorial of 7 is 5040
    Factorial of 8 is 40320
    Factorial of 9 is 362880
    Factorial of 10 is 3628800
    UNIX> 
    
    Factorial has a very clean recursive, mathematical definition: You can program factorial() recursively so that it looks just like that definition. This is in src/fact2.cpp:

    /* Now we write factorial recursively so that it looks just like its mathematical definition. */
    
    long long factorial(long long n)
    {
      if (n <= 0) return 1;
      return n * factorial(n-1);
    }
    

    That feels elegant, doesn't it? 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.

    UNIX> echo 2 3 4 5 6 7 8 9 10 | bin/fact2
    Factorial of 2 is 2
    Factorial of 3 is 6
    Factorial of 4 is 24
    Factorial of 5 is 120
    Factorial of 6 is 720
    Factorial of 7 is 5040
    Factorial of 8 is 40320
    Factorial of 9 is 362880
    Factorial of 10 is 3628800
    UNIX> 
    

    Efficiency

    You should be warned that recursion is not as efficient as using a for() (or while()) loop. An extreme example is that you could implement multiplication of integers a and b by starting with a sum of zero, and adding a to it b times. I have two versions of this below -- the left one uses a while loop, and the right one uses recursion:

    src/mult1.cpp:
    int imult(int a, int b)
    {
      int product;
    
      product = 0;
    
      while (b > 0) {
        product += a;
        b--;
      }
      return product;
    }
    
    src/mult2.cpp:
    int imult(int a, int b)
    {
      if (b <= 0) return 0;
      return a + imult(a, b-1);
    }
    

    Both of them have mains that read in numbers on standard input and multiply them with imult():

    UNIX> echo 10 40 | bin/mult1
    The Product of 10 and 40 is 400
    UNIX> echo 10 40 | bin/mult2
    The Product of 10 and 40 is 400
    UNIX> 
    
    As you can see, they both work; however 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:
    UNIX> echo 10 1000000 | bin/mult2
    Segmentation fault
    UNIX> 
    
    One way to time it is to run both a lot of times. Here, we'll use the scripting language awk to run both of these 1000 times with a=10 and b=100,000:
    UNIX> time sh -c "echo '' | awk '{ for (i = 0; i < 1000; i++) print 10, 100000 }' | bin/mult1 > /dev/null"
    0.242u 0.007s 0:00.25 96.0%	0+0k 0+0io 0pf+0w
    UNIX> time sh -c "echo '' | awk '{ for (i = 0; i < 1000; i++) print 10, 100000 }' | bin/mult2 > /dev/null"
    0.530u 0.009s 0:00.54 98.1%	0+0k 0+0io 0pf+0w
    UNIX> 
    
    As you can see, the non-recursive implementation is more than two times faster than the recursive one.

    What this means is that:


    Standard recursion example: fibonacci numbers

    Fibonacci numbers are a certain class of numbers that have interesting properties (a cool example is how many plants demonstrate Fibonacci sequences -- for example, see https://en.wikipedia.org/wiki/Fibonacci_sequence#/media/File:FibonacciChamomile.PNG). We won't discuss these properties, but we can write programs to calculate them. The definition of fibonacci numbers is recursive: This leads to a very simple recursive implementation of the fibonacci numbers in src/fib1.cpp:

    /* A recursive implementation of the fibonacci numbers.
       The performance of this blows up exponentially. */
    
    #include <iostream>
    using namespace std;
    
    long long fibonacci(long long n)
    {
      if (n <= 1) return 1;
      return fibonacci(n-1) + fibonacci(n-2);
    }
    
    int main()
    {
      long long i;
    
      while (cin >> i) cout << "Fibonacci of " << i << " is " << fibonacci(i) << endl;
      return 0;
    }
    

    It works fine for small values of n:

    UNIX> echo 1 2 3 4 5 6 | bin/fib1
    Fibonacci of 1 is 1
    Fibonacci of 2 is 2
    Fibonacci of 3 is 3
    Fibonacci of 4 is 5
    Fibonacci of 5 is 8
    Fibonacci of 6 is 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 you will learn later, fib(n) = O(1.6)^n, which means that the running time of fib1.cpp is exponential, which is terrible. You'll notice that when you get to values of n in the 40's, fib1 starts slowing down incredibly:
    UNIX> time sh -c "echo 40 | bin/fib1"
    Fibonacci of 40 is 165580141
    0.788u 0.004s 0:00.79 98.7%	0+0k 0+0io 0pf+0w
    UNIX> time sh -c "echo 41 | bin/fib1"
    Fibonacci of 41 is 267914296
    1.249u 0.005s 0:01.25 99.2%	0+0k 0+0io 0pf+0w
    UNIX> time sh -c "echo 42 | bin/fib1"
    Fibonacci of 42 is 433494437
    1.994u 0.005s 0:02.00 99.5%	0+0k 0+0io 0pf+0w
    UNIX> time sh -c "echo 46 | bin/fib1"
    Fibonacci of 46 is 2971215073
    14.538u 0.021s 0:14.57 99.8%	0+0k 0+0io 0pf+0w
    UNIX> 
    

    The problem is making all of those duplicate recursive calls -- think about how many times fibonacci(1) is called in that last run. It doesn't have to be that way -- for example, the implementation in src/fib2.cpp uses a deque to hold fib(i-1) and fib(i) inside a for loop. The loop then iteratively calculates fib(i+1) as the sum of fib(i-1) and fib(i). It pushes the sum on the back of the deque, and then deletes the front. At the end, element 1 of the deque holds fib(n):

    /* We can implement the fibonacci numbers without recursion,
       by keeping track of f(i-1) and f(i-2) to calculate f(i).
       Now, calculating f(n) is O(n) instead of being exponential */
    
    #include <deque>
    #include <iostream>
    using namespace std;
    
    long long fibonacci(long long n)
    {
      deque <long long> f;
      int i;
    
      if (n <= 1) return 1;
    
      /* We start with fib(0) = fib(1) = 1 */
    
      f.push_back(1);
      f.push_back(1);
    
      /* In the body of this for loop, f[0] holds fib(i-1), and f[1] holds fib(i).
         We calculate fib(i+1) by adding f[0] and f[1], pushing it on the back
         of f, and then deleting f[0].  When the loop is done, f[1] will hold fib(n). */
    
      for (i = 1; i < n; i++) {
        f.push_back(f[0] + f[1]);
        f.pop_front();
      }
      return f[1];
    }
    
    int main()
    {
      long long i;
    
      while (cin >> i) cout << "Fibonacci of " << i << " is " << fibonacci(i) << endl;
      return 0;
    }
    

    Now it's a lot faster, because the implementation is O(n) rather than exponential:

    UNIX> echo 1 2 3 4 5 6 | bin/fib2
    Fibonacci of 1 is 1
    Fibonacci of 2 is 2
    Fibonacci of 3 is 3
    Fibonacci of 4 is 5
    Fibonacci of 5 is 8
    Fibonacci of 6 is 13
    UNIX> time sh -c "echo 46 | bin/fib2"
    Fibonacci of 46 is 2971215073
    0.002u 0.003s 0:00.00 0.0%	0+0k 0+0io 0pf+0w
    UNIX> 
    
    We'll revisit this example of exponential recursive blowup in CS302, when we learn Dynamic Programming.

    The Towers of Hanoi

    See this link for a description of the towers of Hanoi (I found this on Swarthmore's web site). This is good conversational fodder for the next time that you are bored at a restaurant -- you can play "Towers of Hanoi" with the onion rings:

    More formally, suppose you have n disks that you want to move from tower 0 to tower 1, and that you are able to use tower 2 in the process. You are only allowed to move a disk at a time, and you can only move a disk from one tower to another if that disk is smaller than the top disk on the destination tower.

    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.


    Implementation

    First, take a look at include/towers.hpp:

    #include <string>
    #include <deque>
    
    class Towers {
      public:
        Towers(int n);                                  // Constructor - n is the number of disks
        std::string Move(int from, int to, bool print); // Move the disk from tower "from" to tower "to"
                                                        // Returns "" if the move was successful.
                                                        // Otherwise, it returns an error string.
        void Print() const;                             // Print the towers as ASCII art.
    
      protected:
        std::deque <int> T[3];                // This holds the sizes of the disks on the three towers
    };
    

    This defines a simple Towers class where each tower is represented by a deque. We'd optimally use a stack, but we want to print the towers, so using a deque ends up being easier. We'll push elements on the front and pop them from the front too. Thus, the constructor and Move() are pretty simple procedures (in src/towers.cpp):

    /* The constructor puts all of the disks onto tower 0.
       The front of the deque is the top of the tower */
    
    Towers::Towers(int n)
    {
      int i;
    
      if (n <= 0) throw ((string) "Towers::Towers() - Bad value of n");
      for (i = 1; i <= n; i++) T[0].push_back(i);
    }
    
    /* Moving is mostly a matter of error checking.  If all is ok,
       we remove the disk from the front of the "from" tower and
       add it to the front of the "to" tower. */
    
    string Towers::Move(int from, int to, bool print)
    {
      int disk;
      /* Error checking */
    
      if (from < 0 || from > 2) return "Bad source tower";
      if (to < 0 || to > 2) return "Bad destiniation tower";
      if (T[from].empty()) return "Source tower is empty";
      if (!T[to].empty() && T[from][0] > T[to][0]) {
        return "Disk on the source tower is bigger than the top of the destination tower";
      }
      
      /* Move the disk and return success */
    
      disk = T[from][0];
      T[to].push_front(disk);
      T[from].pop_front();
      if (print) printf("Moving disk of size %d from tower %d to tower %d\n", disk, from, to);
      return "";
    }
    

    To print the towers, I'm going to use ASCII art. See if you can trace through this one. I'll go over it in class.

    /* Print() creates ASCII art of the towers.  Since we want them to have their
       bases line up, we have to put in a little effort to print the right values
       in the right places. */
    
    void Towers::Print() const
    {
      int max_disk, dots, spaces;
      size_t max_height, i, index;
      int j, k;
    
      /* Calculate the maximum size disk, and the height of the tallest tower. */
    
      max_disk = T[0].size() + T[1].size() + T[2].size();
      max_height = T[0].size();
      if (T[1].size() > max_height) max_height = T[1].size();
      if (T[2].size() > max_height) max_height = T[2].size();
    
      /* Now, go from top to bottom, then left to right, and for each row and
         tower, first figure out if you are printing a disk.  If so, you set
         "dots" to be the size of the disk.  If there is no disk, "dots" is zero.
         Then you'll print "dots" dots, and (max_disk-dots+1) spaces (the extra space
         is to put spaces between the towers. */
    
      for (i = 0; i < max_height; i++) {
        index = max_height - i - 1;
        for (j = 0; j < 3; j++) {
          if (T[j].size() > index) {
            dots = T[j][T[j].size()-index-1];
          } else {
            dots = 0;
          }
          spaces = max_disk - dots + 1;
          for (k = 0; k < dots; k++) printf(".");
          for (k = 0; k < spaces; k++) printf(" ");
        }
        printf("\n");
      }
    
      /* Finally, print the bases of the towers */
    
      for (j = 0; j < 3; j++) {
        for (k = 0; k < max_disk; k++) printf("-");
        printf(" ");
      }
      printf("\n");
    }
    

    Do we need a destructor, copy constructor or assignment overload? The answer is no. Why? Because the deque gets deallocated automatically, and the default copy constructor/assignment overload will copy the deque, which is what we want.

    I have a very simple interactive tower program in src/towers_play.cpp, which starts with all the rings on tower zero, and then allows you to move a ring at a time by entering the source and destination towers. It prints the towers at the beginning and after each move: src/towers_play.cpp

    /* This is a simple command-line player of the towers of Hanoi */
    
    #include "towers.hpp"
    #include <iostream>
    using namespace std;
    
    int main()
    {
      Towers *t;
      int npieces;
      int from, to;
      string rv;
    
      /* Get the number of disks and create the instance of the Towers class. */
    
      cout << "Enter the number of disks: ";
      cout.flush();
      if (!(cin >> npieces)) return 0;
    
      try {
        t = new Towers(npieces);
      } catch (const string s) {
        cout << s; << endl;
        return 0;
      }
    
      /* Print the board, prompt the user, and make the move. */
        
      while (1) {
        t->Print();
        cout << "Enter source and destination: ";
        cout.flush();
        if (!(cin >> from >> to)) return 0;
        rv = t->Move(from, to, false);
        if (rv != "") cout << endl << rv << endl << endl;
      }
    }
    

    Here's a simple example with three rings. You'll note that I make a few mistakes to test my error checking:

    UNIX> bin/towers_play
    Enter the number of disks: 3
    .           
    ..          
    ...         
    --- --- --- 
    Enter source and destination: 0 1
    ..          
    ... .       
    --- --- --- 
    Enter source and destination: 0 1
    
    Disk on the source tower is bigger than the top of the destination tower
    
    ..          
    ... .       
    --- --- --- 
    Enter source and destination: 0 2
    ... .   ..  
    --- --- --- 
    Enter source and destination: 1 2
            .   
    ...     ..  
    --- --- --- 
    Enter source and destination: 0 1
            .   
        ... ..  
    --- --- --- 
    Enter source and destination: 2 0
    .   ... ..  
    --- --- --- 
    Enter source and destination: 015 6
    
    Bad source tower
    
    .   ... ..  
    --- --- --- 
    Enter source and destination: 2 1
        ..      
    .   ...     
    --- --- --- 
    Enter source and destination: 0 1
        .       
        ..      
        ...     
    --- --- --- 
    Enter source and destination: 0 1
    
    Source tower is empty
    
        .       
        ..      
        ...     
    --- --- --- 
    Enter source and destination: bye
    UNIX> 
    

    The recursive solution

    In src/towers_solution.cpp we code up the recursive solution in the Solve() procedure.

    #include "towers.hpp"
    #include <iostream>
    using namespace std;
    
    void Solve(Towers *t, int from, int to, int num_disks)
    {
      int i, other;
    
      /* If there's just one disk on the pile to move, then move it and return. */ 
    
      if (num_disks == 1) {
        t->Move(from, to, true);
        t->Print();
        return;
      }
    
      /* Otherwise, figure out which tower is neither "from" nor "to".  Move the
         top (num_disks-1) disks to that tower recursively, move the bottom disk
         to the destination tower, and then move the (num_disks-1) disks from the
         temporary tower to the destination one. */
    
      for (i = 0; i < 3; i++) if (i != from && i != to) other = i;
      Solve(t, from, other, num_disks-1);
      t->Move(from, to, true);
      t->Print();
      Solve(t, other, to, num_disks-1);
    }
    

    That's a pretty elegant solution, isn't it?

    The main() repeatedly reads in the number of disks, creates an instance of the Towers class, and then solves it recursively:

    int main()
    {
      int npieces;
      Towers *t;
    
      while (1) {
        cout << "Enter the number of towers: ";
        cout.flush();
        if (!(cin >> npieces)) return 0;
        try {
          t = new Towers(npieces);
          t->Print();
          Solve(t, 0, 1, npieces);
          delete t;
          
        } catch (const string s) {
          cout << s << endl;
        }
      }
    }
    

    Let's use it to solve the problem on 1, 2 and 3 disk towers:

    UNIX> bin/towers_solution
    Enter the number of towers: 1
    .     
    - - - 
    Moving disk of size 1 from tower 0 to tower 1
      .   
    - - - 
    Enter the number of towers: 2
    .        
    ..       
    -- -- -- 
    Moving disk of size 1 from tower 0 to tower 2
    ..    .  
    -- -- -- 
    Moving disk of size 2 from tower 0 to tower 1
       .. .  
    -- -- -- 
    Moving disk of size 1 from tower 2 to tower 1
       .     
       ..    
    -- -- -- 
    Enter the number of towers: 3
    .           
    ..          
    ...         
    --- --- --- 
    Moving disk of size 1 from tower 0 to tower 1
    ..          
    ... .       
    --- --- --- 
    Moving disk of size 2 from tower 0 to tower 2
    ... .   ..  
    --- --- --- 
    Moving disk of size 1 from tower 1 to tower 2
            .   
    ...     ..  
    --- --- --- 
    Moving disk of size 3 from tower 0 to tower 1
            .   
        ... ..  
    --- --- --- 
    Moving disk of size 1 from tower 2 to tower 0
    .   ... ..  
    --- --- --- 
    Moving disk of size 2 from tower 2 to tower 1
        ..      
    .   ...     
    --- --- --- 
    Moving disk of size 1 from tower 0 to tower 1
        .       
        ..      
        ...     
    --- --- --- 
    Enter the number of towers: bye
    UNIX> 
    
    One of the things that I love about the Towers of Hanoi is that once you figure out the recursion, the solution nearly writes itself. You don't have to think about where you are moving the pieces, and more specifically, where you move the very first disk -- the recursion takes care of it automatically!

    Can you figure out the number of calls to Make_Move() as a function of n? Let's use those great Unix programs grep and wc to help us:

    UNIX> echo 1 | bin/towers_solution | grep Moving
    Moving disk of size 1 from tower 0 to tower 1
    UNIX> echo 2 | bin/towers_solution | grep Moving
    Moving disk of size 1 from tower 0 to tower 2
    Moving disk of size 2 from tower 0 to tower 1
    Moving disk of size 1 from tower 2 to tower 1
    UNIX> echo 3 | bin/towers_solution | grep Moving
    Moving disk of size 1 from tower 0 to tower 1
    Moving disk of size 2 from tower 0 to tower 2
    Moving disk of size 1 from tower 1 to tower 2
    Moving disk of size 3 from tower 0 to tower 1
    Moving disk of size 1 from tower 2 to tower 0
    Moving disk of size 2 from tower 2 to tower 1
    Moving disk of size 1 from tower 0 to tower 1
    UNIX> echo 3 | bin/towers_solution | grep Moving | wc
           7      77     322
    UNIX> echo 4 | bin/towers_solution | grep Moving | wc
          15     165     690
    UNIX> echo 5 | bin/towers_solution | grep Moving | wc
          31     341    1426
    UNIX> echo 6 | bin/towers_solution | grep Moving | wc
          63     693    2898
    UNIX> 
    
    Let MM(n) be the number of calls to Make_Move() as a function of n. It looks as though:

    MM(n) = 2n-1.

    Can we prove it? Well, it's easy to see that MM(1) = 1. For n > 1, you can see from Solve() that:

    MM(n) = 1 + 2(MM(n-1)).

    So, prove it by induction. If MM(n) = 2n-1 for all values less than n, then :

    1 + 2(MM(n-1)) = 1 + 2(2n-1-1)
    = 1 + 2n-2
    = 2n-1.

    Awesome. Remember that proof. It will be all over those CS31x classes.