Recursion and linked lists

Recursion

Recursion is the definition of something in terms of itself. This sounds circular, but with care, recursive definitions can be a highly effective way to express both algorithms and data structures. Recursion allows us to solve a problem by using solutions to “smaller” versions of the same problem.

Example: Fibonacci numbers

The nth Fibonacci number is the sum of the previous two Fibonacci numbers. This is a recursive definition of a function f(n):

f(n) = f(n–1) + f(n–k)

To make this definition make sense, we need a base case that stops the recursion definition from expanding indefinitely:

f(0) = f(1) = 1

Since the recursive definition is always in terms of smaller values of n, any given f(n) where n≥0 expands into smaller and smaller arguments until the base case is reached. For example:

f(3) = f(2) + f(1) = f(1) + f(0) + f(1) = 1 + 1 + 1

This recursive definition not only makes sense mathematically, it can be implemented in a direct way as Java code:

fibo.java

This is very concise code, but not very efficient, as we'll see.

Example: Exponentiation

Previously we saw an efficient, iterative algorithm for exponentiation. It is even easier to write a similar algorithm recursively:

Exponentiation.java

The base case for the recursion is e = 0, which is clearly computed correctly. For all other values of e, the expression e/2 is smaller than e, so each recursive call gets closer to the base case and so pow must terminate. When evaluating whether it works for a given value of e, we assume that it works for all smaller values of e, in particular for pow(x, e/2). Therefore, the value of h is x⌊e/2⌋ according to the spec for pow. If e is even, then ⌊e/2⌋ = e/2, and h*h is equal to (xe/2)2 = xe, as desired. If e is odd, then ⌊e/2⌋ = (e−1)/2, and h*h*x = x(e−1) * x = xe. So the method works in either case.

Execution of recursive methods

Every time a method is invoked, a structure called an activation record is created in the computer's memory, containing all its local variables, including its parameters. We can represent an activation record in a way similar to the way we've shown objects, as a box containing variables. (However, it's not a full-fledged object, because we cannot create a reference to the activation record.) For example, the diagram below shows the activation records created during the call pow(2,5). Activation records have a pointer to the activation record of the calling method, shown by a dotted arrow in the diagram. As each recursive call occurs, a new activation record is created containing new local variables, so that each distinct call has its own variables.

recursive call stack

When a method returns, a value is returned to the calling method, as shown by the numbers beside the dotted arrows. The activation record of the called method is then destroyed because it is no longer needed.

The activation records of a running program form a stack. A stack is an ordered sequence of items that supports two operations: push and pop. The push operation puts a new item at the beginning of the sequence and pop discards the first item in the sequence. The stack of activation records begins with the current activation record. Each call to a method causes the creation of a new activation record that is pushed onto the stack. Each return from a method causes the current activation record to be popped from the stack. Therefore, activation records are also known as stack frames.

Termination and the base case

For recursive code to be correct, the base case of the recursion must eventually be reached on every chain of recursive calls. Just like in the case of correct loops where we have a decrementing function that gets smaller on every loop iteration, something must get smaller on every recursive call, until the base case is reached.

For example, consider the problem of determining the number of ways to select r items from a set of n items. We write this as C(n,r), where C stands for either choose or combinations. To write the code, we break the problem into deciding what to do with the first element of the set, and then solving the problem recursively for the rest of the set. If we choose the first element of the set of n elements, then there are C(n−1, r−1) ways to pick the remaining elements we need from the rest of the set. If we don't choose the first element of the set, then there are C(n−1, r) ways to pick all the r elements from the rest of the set. Therefore, we have the following equation:

C(n, r) = C(n−1, r−1) + C(n−1, r)
There are two cases where this equation doesn't hold: when r = 0 and when n = r. In both those cases, there is only one way to pick the elements, so:
C(n, n) = C(n, 0) = 1

We can view the space of inputs (n,r) as a table, in which the value of each cell other than the top row and the diagonal is determined by adding the numbers directly above and diagonally to the left. This gives us Pascal's triangle:

 n0 1 2 3 4 ...
r01 1 1 1 1
 1 1 2 3 4
 2 1 3 6
 3 1 4
 4 1

As a decrementing function, we can use the value min(n−r, r), which measures the closest distance to one of the two lines of 1's in the table. If this value goes to zero, then we are in one of the two base cases. Its value decreases by 1 in both recursive calls, so it can never go below zero. Therefore, the base case must be reached along any chain of recursive calls.

Tail recursion and iteration

Earlier we saw that we could code up binary search as an iterative algorithm. We can also implement it recursively, in a way that makes it more obvious why it works.

BinarySearch.java

Why does the algorithm work? First, consider the base case, which is when fst=lst. Since we are assuming that the element k is between fst and lst, it must be at index lst. Otherwise, the code determines whether the element is to the left of m (inclusive) or to the right (exclusive), ensuring both that the precondition of the method is satisfied for the recursive call and that the distance between fst and lst strictly decreases. It can never decrease below zero, so the base case is reached eventually.

The search() method is recursive because it called itself. It is a particularly interesting kind of recursive method, in that it calls itself as the very last thing done: the result of the method is the result of a recursive call. Such a function is said to be tail-recursive.

Tail-recursive methods have an interesting property that they are equivalent to loops. Any tail-recursive method can be converted to a loop and any loop can be converted to a tail-recursive method. The reason this works is that the activation record for a tail-recursive method is not needed after the call is made. So the same activation record can be reused for the recursive call. We wrap the whole method body in a big loop, and instead of passing arguments to the recursive call, we just reassign the formal parameter variables to the new values that we would have passed in the recursive call. Here is what the code looks like after this transformation:

BinarySearchIterative.java

Of course, we can make further simplifications such as folding the base case into the loop guard.

In Java, the iterative version is likely to run more efficiently than the recursive version, because it requires only one activation record. In some languages, such as OCaml, the compiler recognizes tail recursion automatically and generates code that is just as efficient as a loop. Since the transformation to a loop is straightforward, we can write code using tail recursion when that helps us to get the algorithm right, and convert it into an efficient loop when necessary for good performance.

The linked list data structure

We're now going to start talking about data structures, which often involve another kind of recursion: recursion on types. A data structure is simply a graph of objects linked together by references (pointers), often in order to store or look up information.

A classic and still very useful example of a data structure is the linked list. A linked list consists of a sequence of node objects in which each node points to the next node in the list. Some data is stored along with each node. This data structure can be depicted graphically as shown in this figure, where the first three prime numbers are stored in the list.

linked list

The last node in the list does not point to another node; instead, it refers to the special value null or, alternatively, to a sentinel object that is used to mark the end of the list.

A linked list node is implemented with code like the following:

Node.java

Notice that the class Node has an instance variable of the same type. That is, Node is defined in terms of itself. It is an example of a recursive type. Recursive types are perfectly legal in Java, and very useful.

The information in the list may be contained inside the nodes of the linked list, in which case the list is said to be endogenous, or it may merely be referenced by the list node, in which case the list is exogenous. We will be working with exogenous lists here.

A sentinel object can be used instead of the special value null, avoiding the possibility of a null pointer exception:

static Node Null = new Node(); Null.next = Null;

The list shown above is considered a singly linked list because each list node has one outgoing link. It is sometimes helpful to use a doubly linked list instead, in which each node points to both the previous and next nodes in the list. In a doubly linked list, it is possible to walk in both directions.

The definition of a doubly linked list node looks something like the following:

DNode.java

Doubly linked lists come in both linear and circular varieties, as illustrated below. In the linear variety, the first and last nodes have null prev and next pointers, respectively. In the circular variety, no pointers are null. The head node is distinguished by the fact that a pointer is kept to it from outside. The class java.util.LinkedList is actually implemented as a circular doubly linked list.

doubly linked list

Iterative list traversals

We can use lists to build many interesting algorithms. Usually we keep some small number of variables pointing into the list, and follow the pointers between nodes to get around. For example, we can write code to check whether a list contains an object equal to a particular object x:

boolean contains(Node n, Object x) {
    while (n != null) {
        if (x.equals(n.data)) return true;
        n = n.next;
    }
    return false;
}

We can also scan over a list accumulating information. For example, we might compute the total of all the numbers contained in a list of integers:

int total(Node n) {
    int sum = 0;
    while (n != null) {
        sum += n.data;
        n = n.next;
    }
    return sum;
}

Something we often want to do with linked lists is to add a new item into the list. It is easiest to add at the front of the list because the front of the list is immediately accessible. This is sometimes called the cons operation, as in the Lisp language:

Node cons(Object x, Node n) {
    Node ret = new Node();
    ret.data = x;
    ret.next = n;
    return ret;
}

Notice that the new linked list is constructed without making any changes to the existing nodes. In a sense, we now have two linked lists that happen to share most of their nodes. As long as we don't make changes to the nodes of either linked list, this sharing is perfectly okay and something we can exploit for efficiency.

If we want to insert at the end of the list, we have to modify the last node. We can find the last node by scanning down the list until we find it. Or we can keep track of the last node of the list explicitly.

/** Update the list starting at n to add a node containing x at the end.
 *  Requires: n is not null.
 */
void append(Object x, Node n) {
    while (n.next != null)
	n = n.next;
    n.next = new Node();
    n.next.data = x;
}

Building abstractions using linked lists

Linked lists are useful data structures, but using them directly can lead to programming errors. Often their utility comes from using them to implement other abstractions.

Immutable lists

For example, we can use a linked list to efficiently implement an immutable list of objects:

ImmList.java

To implement this interface using a null-terminated list, we will need an additional header object so that we can represent empty lists with something other than null. (Notice that we don't bother to repeat the specifications from the interface. No need!)

ImmListImpl.java

Notice that this implementation allows different lists to share the same list nodes. This makes operations like cons and rest much more efficient than they otherwise would be; the method rest runs in constant time rather than needing to copy all the remaining nodes of the list. It is safe to share list nodes precisely because the list abstraction is immutable, and the underlying list nodes cannot be accessed by any code outside the ImmListImpl class. Abstraction lets us build more efficient code.

Because ImmList is immutable, it makes sense to have an equals operation that compares all the corresponding elements:

boolean equals(Object o) {
    if (!o instanceof ImmList) return false;
    ImmList lst = (ImmList) o;
    Node n = head;
    while (n != null) {
        if (lst.empty()) return false;
        if (!n.data.equals(lst.data)) return false;
        n = n.next;
        lst = lst.rest();
    }
    return lst.empty();
}

Mutable lists

The sharing that was possible with immutable lists is necessarily lost when we use linked lists to implement mutable lists. On the other hand, we can offer a larger set of operations:

MutList.java

Again, this abstraction can be implemented using a linked list. A header object is again handy, especially to keep track of auxiliary information like the number of elements in the list and the last element of the list.

We didn't put a rest() operation in the interface, because it would have to be an O(n) operation. If the client really wants to perform that computation, they will probably copy the whole list and remove the first element of the copy.

Below is the implementation of mutable lists. Notice that prepend and append both can be implemented to take constant time thanks to the last field, which avoids scanning down the whole list to find the end.

A final important thing we want to be able to do with mutable lists is to remove a node. For doubly linked lists, removing nodes is easy, but it is slightly tricky for singly linked lists. The problem is that when the node is found, the previous node in the list needs to be updated to point to the next node. The simple loop we've been using so far will have forgotten what that previous node is. A second wrinkle is that if the node to be removed is the first node in the list, there is no previous node to be updated. We can solve this problem by marching two pointers through the list at the same time. The variable n points to the current node, and the p points to the previous node, or contains null if n is the first node:

MList.java

Abstractions vs. data structures

abstraction barrier

A key observation is that singly or doubly linked lists are merely data structures rather than abstractions. As we've seen, and as depicted on the right, we can use these data structures to implement list abstractions such as immutable lists and mutable lists. As we'll see later on, linked lists are just one of the ways to implement list abstractions. And we can use these data structures, in turn, to implement other abstractions.

For example, one useful abstraction we will see over and over again is the stack. A stack is an ordered list that supports two operations:

The stack abstraction is easily and efficiently implemented using linked lists:

Stack.java

The key is to keep in mind that data structures are ways to implement abstractions, and using them through an abstraction barrier is preferable to using the data structure directly. This allows you to change the data structure without breaking client code.

Recursion on lists

The tail of a list is another, smaller list. That means we can write recursive algorithms that compute over linked lists. For example, we can write the contains method even more compactly using recursion:

/** Returns whether x is in the list starting at node n. */
boolean contains(Object x, Node n) {
    if (n == null) return false;
    if (x.equals(n.data)) return true;
    return contains(x, n.next);
}

Many different computations on lists can be written recursively, including the total method we saw earlier:

/** Returns the total of the data in the list starting at n. */
int total(Node n) {
    if (n == null) return 0;
    return n.data + total(n.next);
}

The code is shorter and simpler than the iterative code. However, it's likely to be a little bit slower, at least in Java. The reason is that the recursive version creates one activation record for each list node, but the iterative version uses only one activation record total.

Tail recursion

Fortunately, many recursive functions can be converted to loops. An example of such a function is contains, above. The key property of contains is that its result in the recursive case is simply the result of a recursive call. In general, a method call that produces a result that is immediately returned (that is, return f(...)) is known as a tail call to f. When a tail call is made, the activation record of the calling method will never be used again. In the case of a recursive tail call, the activation record of the caller can be reused for the callee. This is known as tail recursion.

Java doesn't automatically reuse the activation record in the way that some other languages do. However, we can restructure the code slightly to have the same effect. The trick to reusing the activation record is to wrap the whole function body in a while loop that allow us to restart the call. Then, the recursive call is replaced with assignments that set the formal parameters to the values they would take in the recursive call:

boolean contains(Object x, Node n) {
  while (true) {
    if (n == null) return false;
    if (x.equals(n.data)) return true;
    n = n.next; // x is unchanged in the recursive call
  }
}

As an optimization, we then move the (negated) test n == null into the loop guard, and put the return false after the loop, to get exactly the iterative code above!

boolean contains(Object x, Node n) {
  while (n != null) {
    if (x.equals(n.data)) return true;
    n = n.next; // x is unchanged in the recursive call
  }
  return false;
}

The moral is that we can write short, clear, recursive code and convert it to an efficient loop when efficiency is paramount.