Recursion

Introduction

The joke goes that in order to understand recursion, you must understand recursion. The humor in this seems to lie in two places. First, a recursive function is a function that calls itself, thus understanding recursion to understand recursion seems like a call to itself. Second, that recursion is tough to understand, and so it seems like some people get it and others don't.

I don't believe that. I believe you can understand recursion if you follow a few simple steps. The basis for understanding recursion is to first, understand how to solve a smaller problem, and second, to learn how to trace a recursive function.

Tracing is unbelievably useful when you are just beginning to learn how recursion works. Students mistakenly believe that if a function calls itself, it must behave in some strange and new way. They can't believe that when a function calls itself, it behaves in the same way as if the function called another function.

If you can't properly trace recursion, it's hard to believe it works correctly.

The irony is this: once you have traced recursive functions a few times (say, 10 or 20 times on different functions), then you will begin to trust recursion works, and then, you hardly ever trace code, unless you really need to debug it.

Most people go through three phases when learning recursion. First, they hate it, because they can't understand it. Then, they love it because they understand this mysterious process. Then, they hate it because they think it's inefficient. In particular, recursive solutions often require at least O(n) memory, while iterative solutions can often solve in O(1) space. However, for many problems, the space isn't that big of a concern, and recursive solutions create simple, easy to follow solutions.

Classic Recursion

As defined, recursion occurs whenever a function calls itself. However, there's a more general definition. A set of functions are mutually recursive if there's a way to call the functions such that a function is eventually called again. For example, if function f() calls g() and function g() calls f(), then you have recursion. However, we'll mostly consider the simpler version of a function that calls itself.

However, it's easier to understand recursion this way:

Solving a "big" problem recursively means to solve one or more smaller versions of the problem, and using those solutions of the smaller problems to solve the "big" problem.
In particular, solving problems recursively typically means that there are smaller versions of the problem solved in similar ways. For exmaple, consider the problem of summing values of an array. What's the difference between summing an array of 100 elements versus summing an array of 50 elements?

You use the same technique, but in one case you sum up to 100 elements, and in the other case, you sum up to the first 50 elements. And, even more importantly, you can use the solution to the smaller problem to help you solve the larger problem.

To understand recursion, you must understand that the basic unit of recursion is the function call. In fact, if you avoid using loops and use only recursion, you will find that your function code will generally be much shorter. Writing recursive functions requires you to create more functions that using loops.

The Four Steps to Understanding Recursion

Step 1: Write and define the prototype of the function.

Since functions are the basic unit of recursion, it's important to know what the function does. The prototype you use will dictate how the recursion behaves.

Let's look at an example. Here's a function which will sum the first n elements of an array.

// Sums the first n elements of the array, arr
int sum( int arr[], int n );

Step 2: Write out a sample function call

Once you've determined what the function does, then we imagine a function call.

int result = sum( arr, n );
So, the call is sum( arr, n ). This will sum the first n elements of arr. Pick the most generic function call. For example, you don't want to have a call like:
int result = sum( arr, 10 );
That's picking a constant. You want to use variables when possible, because that's the most general way to call the function.

Step 3: Think of the smallest version of the problem

The smallest version is called the base case. Most people mistakenly pick a base case that's too large. In this case, you will pick a specific value for n.

So, what's the smallest version of the problem? Here are three choices:

sum( arr, 2 );  // Choice 1
sum( arr, 1 );  // Choice 2
sum( arr, 0 );  // Choice 3
Some people pick choice 1, reasoning that if you are to sum elements of an array, then you must have at least two elements to sum. However, that's really not necessary. In math, there is something called a "summation". It's perfectly valid to have a summation of only one element. You just return that one element.

Some people pick choice 2, because it doesn't make sense to sum an array of size 0, whereas an array of size 1 seems to make sense.

However, it turns out choice 3 is the smallest choice possible. You can sum zero elements of an array. What value should it return? It should return 0. As it turns out, 0 is the additive identity. Anything added to 0 is that number. If we wanted to multiply all elements of an array, we would have picked the multiplcative identity, which is 1.

Admittedly, picking such a small value requires a more extensive knowledge of math, but that shouldn't scare you from picking the smallest value possible.

There are several mistakes people make with a base case. The first one we've already mentioned: picking too large a base case. Second, not realizing there may be more than one base case. Finally, thinking that the base case only gets called when the input size is the smallest. In fact, the recursion ALWAYS makes it to some base case. Thus, the base case is where the recursion eventually stops. Don't think of it as merely called when the input is, say, 0. It gets called for all cases (eventually).

Step 4: Think of smaller versions of the function call.

Here's the function call

sum( arr, n )  // sums first n elements of arr
It tries to solves a problem of size "n". We want to think of a smaller problem which we will assume can be solved correctly. The next smallest problem is to sum "n - 1" elements.
sum( arr, n - 1 )  // sums first n - 1 elements of arr
Assume this problem has been solved for you. How would you solve the original, larger problem? If the first n - 1 elements have already been summed then only the n_th element is left to be summed. The n_th element is actually at index n - 1 (because arrays start at index 0).

So, the solution to solving sum( arr, n ) is to add sum( arr, n - 1 ) to arr[ n - 1 ].

Puttting it All Together

Writing a recursive function requires pasting the base case and the recursive case together. Here's the usual format
if ( base case )
   // return some simple expression
else // recursive case
  {
     // some work before 
     // recursive call 
     // some work after 
  }
The simplest version of a recursive function is an if-else statement where the "if" part is the base case, and the "else" part is the recursive case. In the recursive case, there is a recursive call. Most recursive functions do something after the call. After all, you often need the solution of the "smaller" recursive call to create the solution for the "big" problem.

However, on occasion, especially when printing output, you may need to do some work prior to the recursive function call (e.g., printing something).

A more general structure for recursion looks like:

if ( base case 1 )
   // return some simple expression
else if ( base case 2 )
   // return some simple expression
else if ( base case 3 )
   // return some simple expression
else if ( recursive case 1 )
  {
     // some work before 
     // recursive call 
     // some work after 
  }
else if ( recursive case 2 )
  {
     // some work before 
     // recursive call 
     // some work after 
  }
else // recursive case 3
  {
     // some work before 
     // recursive call 
     // some work after 
  }
In this more general version, there may be more than one base case, and there may be more than one recursive case.

To solve the problem from the previous section, we use the simpler of the two versions.

int sum( int arr[], int size )
{
   if ( size == 0 )  // base case
      return 0;
   else
     {
        // recursive call
        int smallResult = sum( arr, size - 1 );

        // use solution of recursive call to solve this problem
        return smallResult + arr[ size - 1 ];
     }
}
Some people don't like multiple return statements. That can be easily handled:
int sum( int arr[], int size )
{
   int result;
   if ( size == 0 )  // base case
      result = 0;
   else
     {
        // recursive call
        int smallResult = sum( arr, size - 1 );

        // use solution of recursive call to solve this problem
        result = smallResult + arr[ size - 1 ];
     }
  return result;
}
You may even think there's no reason to declare smallResult and prefer to write:
int sum( int arr[], int size )
{
   if ( size == 0 )  // base case
      return 0;
   else
     return sum( arr, size - 1 ) + arr[ size - 1 ];
}
Certainly, once you gain more experience with recursive functions, this is the preferable version.

However, declaring a local variable to store the result of the recursive call helps beginners to think about the "small" solution and then thinking about how to use that "small" solution to solve the bigger problem.

Classic recursion involves thinking "backwards". Instead of building a solution from nothing, you pretend you are at the solution, and want to take a step back and ask how to solve the problem if you were a step back.

Here's an analogy. You are planning a trip from point A to point B. One way to start is to begin at point A and move forward to B. Most people like that solution and find it easier to think that way.

However, another approach is to be at B and step back one step towards A (let's call this point, C), and assume that you can reach C, and figure out how to B once you reach C. For example, you might be travelling from Atlanta to Boston. You think "if I were at New York, how would I make it to Boston" and then worry about how to solve the problem of getting from Atlanta to New York.

If you learn to think "backwards" or more accurately, learn to figure out what the correct "smaller" version of the problem is, you're well on your way to figuring out recursion.

Tracing Recursion: Key to Understanding

Because tracing recursion is so important, here's a link to it's own webpage.

Tracing Recursive Functions

Backward and Forward Recursion

To me, classic recursion uses "backward" recursion. Backward recursion is the following idea. You want to solve a problem of size N. So, assume a solution of a smaller size, say, N - 1. Use that solution to solve the problem of size N.

In classic recursion, you avoid, as much as is reasonably possible, global variables and reference parameters. You