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-2) \)

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

\( f(0) = 0 \qquad 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 + 0) + 1 = 2 \)

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

An efficient algorithm for exponentiation known as “squaring and multiplication” can be implemented recursively in an elegant way. Algorithms like this one are used to implement the exponentiation needed by modern cryptography, because the exponents are too large for the obvious, simple exponentiation algorithm to be practical.

Exponentiation.java

It is not difficult to see that this code works correctly. 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). Since the expression e/2 computes \( \lfloor e/2 \rfloor \), the value of h is \(x^{\lfloor e/2 \rfloor}\) according to the spec for pow. If e is even, then \( \lfloor e/2\rfloor = e/2\), so h*h is equal to \( (x^{e/2})^2 = x^e\), as desired. If e is odd, then \(\lfloor e/2\rfloor = (e-1)/2\), so h*h*x = \(x^{(e-1)} · x = x^e\). 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.

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:

 \(n\)0 1 2 3 4 ...
\(r\)01 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.

Linked lists

We are now going to start talking about data structures, objects used primarily to store and look up information. Data structures often involve another kind of recursion: recursion on types.

A classic and 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.

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. A singly linked list can only be traversed in one direction, from front to back. 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, at a cost of more storage to represent the extra links.

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 traversal

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 sum of all the numbers contained in a list of integers:

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

or more succinctly,

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

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

Abstract data types vs. data structures

abstraction barrier

A key observation is that singly or doubly linked lists are concrete data structures. They are implementations of a particular set of operations. The operations are usually specified in Java by an interface. The collection of operations and their specifications is called an abstract data type (ADT). For example, immutable lists and mutable lists are abstract data types, because they are merely specifications of a collection of operations. These abstract data types can be implemented by linked lists, but as we will see later on, linked lists are only one of the ways to implement list abstractions.

We can also use data structures such as linked lists to implement other abstractions. For example, one useful abstract data type we will see many times over is the stack. A stack is a list that supports two operations:

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

Stack.java

Keep in mind that data structures are ways to implement abstract data types, 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 sum method we saw earlier:

/** Returns the sum of the data in the list starting at n. */
int sum(Node n) {
   if (n == null) return 0;
   return n.data + sum(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 elimination

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 recursive version of search() above is a particularly interesting kind of recursive method in that it calls itself as the very last action. It does not do any computation after the call, but simply returns the result of the recursive call. Such a call is called a tail call. A recursive method with only tail calls is said to be tail-recursive.

Tail-recursive methods have an interesting property: they are equivalent to while loops. Any tail-recursive method can be converted to a while loop and vice versa. 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 do not need a stack of activation records, but can do with just one.

To convert a tail-recursive method to a while loop, we wrap the method body in a big while loop with guard true, then replace each recursive call with assignment statements that reassign to the method's formal parameters the new values that would have been passed in the recursive call. Here is what the binary search 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.

Another example of a tail-recursive method is the contains method above. It is tail-recursive because all recursive calls are tail calls. Here is the result of the transformation to a while loop.

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 often write short, clear, recursive code and convert it to an efficient loop when efficiency is paramount.