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:
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.
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.
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.
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.
|
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.
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.
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.
|