Recursion on Linked Lists

Advanced Programming/Practicum
15-200


Introduction In this lecture we will extend our study of recursion by studying various recursive methods operating on linked lists. This is natural, because linked lists can themselves be defined recursively:
  1. A null reference is an empty linked list.
  2. A non-null reference to an object (from class LN) whose next instance variable refers to any linked list (either empty or not) is a non-empty linked list.
Recursive methods operating on linked lists are often simpler to write and easier to understand then their iterative counterparts (once we become familiar with recursive methods). They often store no state (nor use any state change operators). Next week, when we begin our study trees, we will find that some methods can be written only via recursion. So we are using recursion on linked lists to bridge the gap between recursion and trees.

We will first examine recursive accessor methods that process linked lists. Then we will examine recursive mutator methods that add or remove values from linked lists; these methods are written using a simple pattern that Java programmers should learn. Finally, we will discuss how to write recursive methods in collection classes We will use a simple pattern here too, which requires writing one public method that calls a private recursive helper method.


Recursive Accessor Methods Most recursive methods operating on linked list have a base case of an empty list; most have a recursive call on the next instance variable, which refers to a smaller list: one that contains one fewer node. Probably the simplest recursive method is one that returns the number of nodes in a linked list (the length of the list). Its code is shown below.
  public static int length (LN l)
  {
    if (l == null)
      return 0;
    else
      return 1 + length(l.next);
  }
Note the proof of correctness.
  • For the base case (an empty list) this method returns the correct length: 0.
  • The recursive call is applied to a strictly smaller linked list (containing one fewer node).
  • Assuming length(l.next) correctly computes the length of the list following the first node, then returning a value one bigger correctly computes the length of the entire list.
By changing this method ever so slightly, we can compute the sum of all the values in the nodes of a linked list.
  public static int sum (LN l)
  {
    if (l == null)
      return 0;
    else
      return l.value + sum(l.next);
  }
The proof of correctness is also similar.

The following two methods print the values in a list: the first in the standard order, the second in reverse order. Notice that the only difference between the two methods (besides their different names) is the order of printing the node's value and the recursive call.

  public static void print (LN l)
  {
    if (l == null)
      return;
    else {
      System.out.println(l.value);
      print(l.next);
    };
  }
This method is simple to write iteratively for linear linked lists. But now we discuss the method that prints all the values in reverse order. This method seems impossible to write for linear linked lists, because after we printe a node, we must print the node that comes before it (and there is no link to the previous node in linear linked lists).
  public static void reversePrint (LN l)
  {
    if (l == null)
      return;
    else {
      reversePrint(l.next);
      System.out.println(l.value);
    };
  }
Yet, we can write this method as easily as print, if we use recursion. Note the proof of correctness.
  • For the base case (an empty list) this method prints it in reverse order correctly: no values
  • The recursive call is applied to a strictly smaller linked list (containing one fewer node).
  • Assuming reversePrint(l.next) correctly prints (in reverse order) all the values in tha list after the first node), then printing the first node afterwards correctly prints all the values in the list in reverse order.
This illustrates that recursion as strictly more powerful than iteration: an interative method can be converted into a recursive one; but there are recursive methods (like printReversed) that cannot be translated into iterative code UNLESS AN EXTRA COLLECTION CLASS IS USED. Typically, iteration+stack is equivalent to recursion. For example, we can write reversePrint iteratively as follows
  public static void reversePrint (LN l)
  {
    Stack s = new ArrayStack();
    for (LN r=l; r!=null; r=r.next)
      s.add(r);
    while (!s.isEmpty())
      System.out.println( ((LN)s.remove()).value);
  }
Such a conversion (recursive -> iterative+stack) is not alsways so easy.

Finally, we can also simplify methods such as these (involving an immediate return in the base case) as:

  public static void reversePrint (LN l)
  {
    if (l != null) {
      reversePrint(l.next);
      System.out.println(l.value);
    };
  }
Although containing less code (there is an implicit return in this void method), this code doesn't match the standard recursive form, as it does not check for the base case, nor should (explicitly) what to do in this case. You can choose which form to follow when you write your code.

The following method performs a linear search and returns either null or a reference to the node containing value.

  public static LN search (LN l, int value)
  {
    if (l == null)
      return null;
    else
      if (l.value == value)
        return l;
      else
        return search(l.next, value);
  }
Note the proof of correctness.
  • For the base case (an empty list) this method returns the correct answer: null, because there are no nodes in the linked list containing value (in fact, there are no nodes at all).
  • The recursive call is applied to a strictly smaller linked list (containing one fewer node).
  • Assuming search(l.next) correctly returns a reference to a node containing value from the list following the first node, then this method returns the correct reference: it either returnins a reference to the first node (if it storesvalue) or if not it returns the result of the recursive call.
We can also simplify this method by combining the base case with the case of finding the correct reference to return, and write it as follows.
  public static LN search (LN l, int value)
  {
    if (l == null || l.value == value)
      return l;
    else
      return search(l.next, value);
  }
Finally, here is a particularly elegant recursive way to copy a linked list. It is as efficient as the iterative method that we studied (which required a cache reference) and much simpler to write and prove correct.
  public static LN copy(LN l)
  {
    if (l == null)
      return null;
    else
      return new LN(l.value,copy(l.next));
    }
  }
Note the proof of correctness.
  • For the base case (an empty list) this method returns the correct answer: null (a "copy" of that empty list); there is no node in the linked list containing a value.
  • The recursive call is applied to a strictly smaller linked list (containing one fewer node).
  • Assuming copy(l.next) correctly returns a reference to a copy of a linked list containing all the nodes after the first one, then this method correctly returns a copy of the entire list by returning a reference to a copy of the first node, whose next is a reference to a copy of all nodes following the first.

    Note that the complexity classes in all these cases is O(N), because in the worst case, there is a recursive call for all N nodes that a linked list contains.


Recursive Mutator Methods Recursive mutator methods follow a pattern in which they return a reference to the mutated linked list (instead of being void); such a generalization allows for a simple recursive implementation of the method. This approach takes a bit of getting used to, but it is a pattern that is used repeatedly here and in the recursive processing of tree with mutators. In the following method, it used used to return a reference to the linked list l, in which a new node containing value is inserted at its rear.
  public static LN insertRear (LN l, int value)
  {
    if (l == null)
      return new LN(value,null);
    else {
      l.next = insertRear(l.next, value);
      return l;
    }
  }
We call this method like front = insertRear(front, 5);

Note the proof of correctness.

  • For the base case (an empty list) this method returns the correct answer: a reference to the original linked list (which is empty!) in which a new node containing value is inserted at its rear
  • The recursive call is applied to a strictly smaller linked list (containing one fewer node).
  • Assuming inserRear(l.next,value); correctly returns a reference to the linked list l.next (containing all nodes after the first), in which a new node containing value is inerted at its rear; then storing a reference to this list into l.next and returning l is correctly returning a reference to the linked list l, in which a new node containing value is inerted at its rear.
The last part of this proof is definitely complicated and subtle. To be understood, it will require some thinking (and possibly some hand simulation).

The code below is another way to write this same method. We might consider this code simpler because it has only one return at the end, returning a reference to either a new node or the node it was passed.

  public static LN insertRear (LN l, int value)
  {
    if (l == null)
      l = new LN(value,null);
    else 
      l.next = insertRear(l.next, value);

    return l;
  }

We can extend this pattern ever-so-slightly to insert a value into an ordered list. Here we rely on short-circuit evaluation of ||.

  public static LN insertOrdered (LN l, int value)
  {
    if (l == null || value < l.value)
      return new LN(value,l);
    else {
      l.next = insertOrdered(l.next, value);
      return l;
    }
  }
We call this method as front = insertOrdered(front, 5); Compare this recursive method with the iterative one that solves the same task.

Likewise, the code below is another way to write this same method, again using just one return.

  public static LN insertOrdered (LN l, int value)
  {
    if (l == null || value < l.value)
      l = new LN(value,l);
    else 
      l.next = insertOrdered(l.next, value);

    return l;
  }

Finally, here are two methods for removing values from a linked list. The first removes just the first occurrence of that value; the second removes all occurrences.

  public static LN removeFirst(LN l, int value)
  {
    if (l == null)
      return null;
    else
      if (l.value == value)
        return l.next;
      else {
        l.next = removeFirst(l.next, value);
        return l;
      }
  }
We call this method as front = removeFirst(front, 5);

As we did above, we can "simplify" this method to.

  public static LN removeFirst(LN l, int value)
  {
    if (l != null)
      if (l.value == value)
        l = l.next;
      else
        l.next = removeFirst(l.next, value);

    return l;
  }
Note that if l is null, this method just returns null.

The second version, below, is identical to the first, except for a true test in the inner if statement it returns removeAll(l.next) instead of just l.next. Thus, in this version value is recursively removed from the smaller linked list as well. In these recursive methods, compared to the iterative ones, small changes in the semantics of an operation often lead to small changes in the code.

  public static LN removeAll(LN l, int value)
  {
    if (l == null)
      return null;
    else
      if (l.value == value)
        return removeAll(l.next,value);
      else {
        l.next = removeAll(l.next, value);
        return l;
      }
  }
We call this method as front = removeAll(front, 5);

As we did above, we can "simplify" this method to.

  public static LN removeAll(LN l, int value)
  {
    if (l != null)
      if (l.value == value)
        l = removeAll(l.next, value);
      else
        l.next = removeAll(l.next, value);

    return l;
  }

Finally, notice that one form of inefficiency in recursive calls is that a parameter like value must be repeatedly passed/copied from one method call to the next. A smart Java compiler can recognize this phenomenon and generate code that does the equivalent but without the overhead of actually passing this value.


Recursive Methods in Collections When implementing collection classes with recursive methods, we typically must write a pair of methods for each operation.
  1. The first method is the public one specified in the interface. It can be written iteratively or recursively. It written iteratively, it simply calls ...
  2. The second method, which is a private static one that does all the work.
For example, suppose that we are implementing a generic priority queue (LN stores value as an Object) via a linked list, using a front instance variable and a priorityComparator instance variables. We would implement the add method in this class with the following pair of methods.
  public void add (Object o)
  {front = add(front,o);}

  private static LN add (LN l, Object o)
  {
    if (l == null || priorityComparator.compare(l.value,o) < 0)
      return new LN(value,l);
    else {
      l.next = add(l.next, o);
      return l;
    }
  }
Sometimes we will use the same name, here add for the second method, overloading the public method with a different private prototype for the helper method. Other times we might use the standard name for this method (discussed above): insertOrdered.

We will explore in more detail this pattern of paired methods when we discuss processing trees.


Problem Set To ensure that you understand all the material in this lecture, please solve the the announced problems after you read the lecture.

If you get stumped on any problem, go back and read the relevant part of the lecture. If you still have questions, please get help from the Instructor, a CA, or any other student.

  1. Examine the method below
      public static boolean equalLength(LN l1, LN l2)
      {return length(l1) == length(l2);}
    Although this method is trivial to write, it takes a long time to execute when passed a very large linked list (a million node list) and a very short one (a 5 node list), because it computes the length of each separately, and computing the length of the large list takes much more time than computing the length of the small list. Rewrite this method directly using recursion so that it only traverses as much of each list as necessary to compute a result (when it tries to reach the 6th node of each list -and fails for the smaller list- it knows the answer. Hint: deal with the following four cases: both l1 and l2 are empty, l1 is empty and l2 is not empty, l1 is not empty and l2 is empty, and finally both l1 and l2 are not empty; in three of four cases, an answer can be returned immediately (the other requires recursion).

  2. Write the public static method named merge, which takes two parameters: each is a reference to a sorted (in increasing order) list. This method returns a reference to a new sorted list, which copies all the values from its two parameter lists (which remain unchanged). Hint: use a recursive structures limilar to the equalLength method described above.

  3. Familiarize yourself with the methods covered in this lecture: know how to prove/hand-simulate them.