19.2 Recursion

Specification

Introduction

Files & Resources

Recursion is simply the ability for a procedure to call itself in order to complete a task.  Recursion is also commonly used in datatypes as well as some formal languages such as Reverse Polish Notation.

Recursion is a method of solving problems based on the divide and conquer mentality. The basic idea is that you take the original problem and divide it into smaller (more easily solved) instances of itself, solve those smaller instances (usually by using the same algorithm again) and then reassemble them into the final solution.

This Wikipedia page contains a good amount of information, and a lot more besides.  There are also the lesson PowerPoints at the foot of this page.

Components

A recursive routine has several parts to it:

The base case is an essential component of the routine, which prevents an infinite loop.  Very often, you also need more than one base case, including trapping any errors that could also cause an infinite loop.  Although in reality, infinite loops would be impossible as the computer would most probably run out of memory.

Another way to look at this is the general case is where the solution is expressed in terms of a smaller version of itself. In other words, here, the problem space is made smaller and smaller. (the smaller problem space is sent to the calling function) base case--the case for which the solution can be stated nonrecursively. The base case is the smallest version of the problem for which we can calculate an answer. With factorials, knowing the factorial of 1 allows us to then unwind.

When to use

The rule of thumb for recursion is, "Use recursion, if and only if on each iteration your task splits into two or more similar tasks".  Fibonacci is not a good example of recursion application, while the Towers of Hanoi is a good one.

So most of the good examples of recursion are tree traversal in different disguises.

For example: graph traversal - the requirement that visited node will never be visited again effectively makes graph a tree (a tree is a connected graph without simple cycles).  Divide and conquer algorithms (quick sort, merge sort) - parts after "divide" constitute children nodes, "conquer" constitutes edges from parent node to child nodes.

Examples

The following code, written in Java, counts the number of files within a file subsystem.  This type of program would be much more difficult implemented as a non-recursive routine.  Using this example, you can see how the use of recursion allows us to break each successive folder to complete the same task.

public static int countFiles(File f) {

    if (f.isFile()){

        return 1;

    }

    // Count children & recurse into subdirs:

    int count = 0;

    File[] files = f.listFiles();

    for (File fileOrDir : files) {

        count += countFiles(fileOrDir);

    }

    return count;

}

Drawbacks of recursion

In the majority of major imperative language implementations (i.e. every major implementation of C, C++, Basic, Python, Ruby,Java, and C#) iteration is vastly preferable to recursion.

To see why, walk through the steps that the above languages use to call a function:

Doing all of these steps takes time, usually a little bit more than it takes to iterate through a loop. However, the real problem is in step #1. When many programs start, they allocate a single chunk of memory for their stack, and when they run out of that memory (often, but not always due to recursion), the program crashes due to a stack overflow.

So in these languages, recursion is slower and it makes you vulnerable to crashing. There are still some reasons why recursion is still useful and necessary, and in general, code written recursively is shorter and a bit more elegant once you know how to read it.

There is a technique that language implementers can use called tail call optimization, which can eliminate some classes of stack overflow. Put succinctly: if a function's return expression is simply the result of a function call, then you don't need to add a new level onto the stack, you can reuse the current one for the function being called. Regrettably, few imperative language implementations have tail-call optimisation built in.