Recursion
Steven J. Zeil
Most of the time, when you want to process a lot of data, you use a loop. Recursion is an alternative that you could employ, but if you’re like most programmers, you’re a lot more comfortable with looping or iteration than you are with recursion. That can be a problem, in some circumstances, because there are some tasks that really are more easily solved via recursion.
A function is recursive if it calls itself, or calls some other function that eventually causes it to be called again. This means that a recursive function may have several different calls to it active at the same time. In fact, we call the collection of information that represents a function call in progress an activation of the function.
Recursive functions tend to fall into certain familiar patterns. Every recursive function will test its inputs to see if they represent a special case simple enough to be solved without recursion. These are called base cases. When the function’s inputs do not constitute a base case, the recursive function must somehow break apart the problem to be solved into one or more smaller sub-problems. These sub-problems are solved via recursive calls, and when the recursive calls are done the function must combine the sub-problem solutions into a solution for the original problem.
1 Overview
Here is a simple example of a recursive function.
template <class Iterator>
int length (Iterator start, iterator stop)
{
if (start == stop) // range is empty: base case
return 0; // base case solution
else
{
++start; // shorten the range
return 1 + length(start, stop);
}
}
This one uses recursion to compute the length of a range of positions. (We’ll ignore, for now, the fact that there are easier ways to do this.) As simple as this example is, it illustrates all the important characteristics of a recursive function.
-
The base case occurs when the range is empty. In that case, we can compute the length of the range immediately, without recursion or iteration, because we know that an empty range has length zero.
-
If the range is not empty, we need to take the problem apart into a smaller sub-problem. We can do this by computing the length of the range left when we remove the first element of the current range. We use a recursive call to get the length of this short-term range, and then add one to it to get the length of the original range.
2 You’ve Gotta Believe!
In an earlier edition of your textbook, Weiss suggested the that understanding a recursive function is a bit like attending an old time gospel meeting: “you’ve gotta believe!”
template <class Iterator>
int length (Iterator start, iterator stop)
{
if (start == stop) // range is empty: base case
return 0; // base case solution
else
{
++start; // shorten the range
return 1 + length(start, stop);
}
}
Question: How do we know next the recursive call on the shorter range really will return the correct value?
That may seem like a lot of work, but it’s not as if iterative functions were a whole lot easier to get right. When we write an algorithm using loops, we need to convince ourselves that the loop processes each individual element correctly, that the processing of the individual elements adds up to a solution for the entire problem, that the loop condition is correct and will cause us to go around the loop the correct number of times, that the entire loop will function correctly even in the case where we go around the loop zero times (if that is possible), and finally we must convince ourselves that the loop will eventually exit, and not go on looping forever.
3 Recursion versus Iteration
Recursion and iteration (looping) are equally powerful. We know, for example, that any recursive algorithm can be rewritten to use loops instead. We know this is true because that’s how recursion is implemented on the underlying machine. Your text describes how computer systems use a runtime stack (called the activation stack) to keep track of the return addresses, actual parameters, and local variables associated with function calls. Each function call actually results in pushing an activation record containing that information onto the stack. Returning from a function is accomplished by getting and saving the return address out of the top record on the stack, popping the stack once, and jumping to the saved address.
In a sense, then, computers really don’t do recursion. What we might write as a recursive algorithm really gets translated as a series of stack pushes followed by a jump back to the beginning of the recursive function, all implemented using the underlying CPU whose internal code is, fundamentally, iterative.
We can go the other way as well. Given an algorithm with loops, we could, without too much trouble, replace each loop body with a recursive function that would perform a single iteration of the original loop, check to see if the loop would terminate, and if not call itself recursively to simulate the next time around the loop.
4 Making the Choice
If neither recursion nor iteration is fundamentally more powerful than another, how do we decide which to use?
-
Some algorithms are just easier to write one way than another. The “length of a range” function would have been easier to write with a loop. Later we will encounter some searching and sorting algorithms that are so naturally recursive that it’s hard to imagine doing them iteratively, and any iterative form would be far more difficult to understand.
-
Some languages support recursion but not looping. Some languages have loops but not recursion. Although such programming languages are increasingly rare, in those cases, our choice may be made for us.
-
Iteration is usually (though not always) faster than an equivalent recursion. For example, binary search can be written recursively, as shown in the loop-free version here:
int binSearch (const int arr[], int first, int last, int target) // search for target in ordered array of data // return index of target, or index of // next smaller target if not in collection { int mid; // index of the midpoint int midValue; // object that is assigned arr[mid] int origLast = last; // save original value of last // Reduce the area of search // until it is just one target if (first < last) { // test for nonempty sublist mid = (first + last) / 2; midValue = arr[mid]; if (target == midValue) return mid; else if (target < midValue) return binSearch (arr, first, mid, target); // search lower sublist else return binSearch (arr, mid+1, last, target); // search upper sublist } else return origLast; }
But that’s not any simpler and will run noticably slower. (Not in a big-O sense. The complexity of the recursive and iterative forms are the same, but the multiplicative constants for recursion will be higher.)
-
Other performance/environmental issues may come into play.
In some operating systems, the system’s activation stack is very small. You may see this in programming for embedded systems, small computer systems used to control devices (e.g., microwave ovens, aircraft displays, automotive computers that control engine timing, fuel injection, etc.). Such systems generally have very limited memory overall, including very limited activation stacks.
On such systems, recursion may be out of the question.