Recursion In this lecture we will discuss some recursive methods that operate on integers and Strings, and then progress to recursive methods that operate on linked lists (and observe the related recursive definition of linked lists). We will first cover methods operating on recursive linked lists that do NOT change the structure of the list, and then move to recursive methods whose purpose is to CHANGE the structure of the linked list, learning common idioms for each. Simple Recursion: In the web reading (a powerpoint presentation), we will look at the general form of all recursive methods and try to get an intuitive undestanding of recursion and constrast it to iteration. Recursion is actually a more powerful control structure than iteration, and recursion applied to non-linear linked structures (like trees and graphs) is a very powerful programming technique. We will start by examining a recursive definition for the factorial function (e.g., 5! reads as "five factorial") in the reading. The definition itself is recursive: 0! = 1 and for all N>0, N! = N*(N-1)!; here we define N factorial in terms of a (N-1)!. The code will first appear according to the structure that we use for all recursive methods, which we will simplify to public static int factorial (int n) { if (n==0) return 1; else return n*factorial(n-1); } The web reading contrasts this code with the iterative code that implements this same method, noting that the iterative code requires several state change operators while the recursive code does not. State change operators make it hard for us to think about the meaning of code (tough to prove that the code is correct), and makes it hard for multi-core processors to coordinate in solving a problem. "Functional programming languages" are more amenable to be automatically parallelizable (can run more quickly on multi-core computers). You'll see more about this in later classes at UCI (Concepts of Programming Languages). In fact, we can use a conditional expression to write this code very compactly -as a one-liner; public static int factorial (int n) { return (n==0 ? 1 : n*factorial(n-1); } Next, we will see how to hand-simulate a recursive method using a "tower of call frames" in which each resident executes the same code: he/she is called by the resident above and calls the resident underneath, when a recursive call is needed (calling back the resident above when their answer is computed). Truth be told, while it is useful to be able to hand-simulate a recursive call, hand-smulation is not a good idea for understanding or debugging recursive methods. Instead, we will learn how to verify that recursive methods are correct by three proof rules. Even more important than proving that existing methods are correct (to better understand them), we will use these same three proof rules to guide us when we synthesize new recursive methods. Note that in direct recursion (when a method calls itself) we say that the method "recurs", not that it "recurses". Recurses describes what happens when you bang your toe into a door the second time. The three rules require should be fairly simple to apply in most cases. 1) Prove that the base case is processed correctly. Should be easy because base cases are small and simple. 2) Prove that each recursive call gets closer to the base case. Should be easy because there are "standard" ways to recur: ints go down by 1 or a factor of 10 (i.e., x/10 has one fewer digit); Strings recur on a substring (fewer characters); lists recur on x.next, which is a linked list smaller by one node (closer to the empty list, which is the typical base case for lists); trees recur on t.left and t.right (smaller subtrees). 3) ASSUMING ALL RECURSIVE CALLs SOLVE THEIR SMALLER SUBPROBLEMS, prove what the code combines these solutions to solve the original problem (combines solved subproblems to solve the original problem). Should be easy, because we get to assume something very important and powerful: all subproblems are correctly solved. There are proofs of methods that go along with both of the web readings. We can use these rules to synthesize a method that reverses a String. I noted that it often takes a bit of extra work to ensure you choose the SMALLEST base case, but think hard about getting this part right. Students often mess-up solving the smallest base-case. If we want to write the the method public static String reverse (String s) We start by determining the smallest String (the empty String, which has 0 characters, not a String with 1 character). Of course, the reverse of the empty String is just the empty String so we can start by writing public static String reverse (String s) { if (s.equals("")) //or s.length() == 0 return ""; else recur to solve a smaller problem and use the solution of the smaller problemto solve the original problem } We can guess the form of the recursion as reverse(s.substring(1)); s.substing(1) returns a String with all characters but the one at index 0: all characters after the first. We can call this method only on non-empty Strings (those have at least 1 character), which is guaranteed by failing the base-case test in the if statement. public static String reverse (String s) { if (s.equals("")) return ""; else use reverse(s.substring(1)) to solve the original problem } Now, think about an example. if we called reverse("abcd") we get to assume that the recursive call works: so reverse(s.substring(1)) is reverse("bcd") which we get to assume returns "dcb"). How do we use the solution of this subproblem to solve the original problem: we need to catenate "a" (the first character) at the end of the reverse of all the other characters, which gives us the reverse of all the characters. Generally we write this as public static String reverse (String s) { if (s.equals("")) return ""; else return reverse(s.substring(1)) + s.charAt(0); } We have now written this method by ensuring the three proof rules are satisfied. Note that the smallest String is reversed correctly; the recursive call is on String smaller than s (all the characters from index 1 to the end, skipping index 0), and ASSUMING the recursive call works correctly for the smaller String, then by catenating the first character on the end of it, we have correctly reversed the entire String (solving the original problem). In fact, we can use a conditional expression to write this code as well, but since it is a bit complex, I don't think this version, which looks a bit dense, has any advantage over the original. public static String reverse (String s) { return (s.equals("") ? "" : reverse(s.substring(1)) + s.charAt(0) );} I skipped some of the other examples in the first web reading, but you are welcome to examine them. Recursion on Linked Lists: Linked lists have a natural, recursive definition: 1) An empty list (the smallest linked list) is a null reference 2) Any non-empty list is a reference to an object (from class LN) whose next instance variable refers to some smaller linked list (either empty or not) Using this defintion as a guide, we can often write linked-list processing code recursively. This definition suggests an idiom for writing recursive methods, treating an empty list as the base case. We start with a method that recursively computes the length of any linked list (the number of nodes it contains), using the standard recursive form and the empty base case: static int length (LN l) { if (l == null) return 0; else return 1 + length(l.next); } The web reading shows a proof that this method is correct. This method has an iterative version that is just as simple, although it does involve state changes to the local variables count and r. static int length (LN l) { int count = 0; for (LN r = l; r!=null; r=r.next) count++; return countp; } The notes show some simple variants of methods that recursively procss all the values in a list: to sum up all the values and to print all the values. The print method is given as public static void print (LN l) { if (l == null) return; else { System.out.println(l.value); print(l.next); } } Printing the base case (an empty list) prints nothing, otherwise we print the value of the first node in the list and then recursively print all values after it. What is interesting about this method is that a small change to the code (reversing the order of the System.out.println and recursive call) leads to a big change in what the method does: it prints the list in the reverse order. This is a task that we cannot do easily iteratively. The best we can do iteratively is reverse the list, then print it, then reverse it again. Another option is to put all the values in a stack and then empty the stack, printing the values last to first. Contrast this with a hand-simulation of this code, which uses the call-frame stack to get the job done in a similar way, but with no explicit stack or stack operations. Here is the recursive code and a proof that it works. public static void reversePrint (LN l) { if (l == null) return; else { reversePrint(l.next); System.out.println(l.value); } } 1) For the base case (an empty list) this method prints it in reverse order correctly, because it prints no values. Note: don't just say the base case works correctly; say what the base case is (an empty list), say what it does (prints no values), and say why it is correct (an empty list printed in reverse still prints no values). 2) The recursive call is applied to a strictly smaller linked list l.next (containing one fewer nodes). 3) Assuming reversePrint(l.next) correctly prints (in reverse order) all the values in the list after the first node), then printing the first node afterwards correctly prints all the values in the list in reverse order. The web reading shows two variants of searching for a value in a linked list and returning a reference to the first node that stores such a value, if there is one (or returning null otherwise). Next, the web reading shows a very elegant method to produce a copy of a linked list. static LN copy (LN l) { if (l == null) return null; else return new LN(l.value, copy(l.next)); } 1) For the base case (an empty list) this method returns the correct answer: null (a "copy" of the empty list is an empty list). 2) The recursive call is applied to a strictly smaller linked list, l.next (containing one fewer node). 3) 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 (returned correctly by the recursive call). Contrast this with the iterative methods that we used previously. The best iterative code for this method is not so simple (or easy to understand), although getting used to reading recursive methods does take a bit of time. Note that the complexity of both methods is O(N): N iterations vs. N recursive calls. There are lots of variables and state changes in the code below. static LN copy (LN l) { LN front = null, rear = null; for (LN r = l; r!=null; r=r.next) if (front == null) front = rear = new LN(l.value,null); else rear = rear.next = new LN(l.value,null); return front; } Next we will look at a method that recurs on two linked lists: it determines whether the two linked lists are "equal" (have the same number of nodes each storing the same values in the same order). Note that this version DOESN'T compute the length of either list first: it recurs down both list so long as each contains another value to check for equality. There are four cases (3 of which are base cases, allowing an immediate answer to be returned): list l2 null non-null +----------+------------+ null | equal | not equal | List l1 +----------+------------+ non-null| | not equal| check/recur| +----------+------------+ So, if either list is null (or both are null), we know the answer in three of these four cases: the lists are equal only if both are null; if one is null and one isn't null, then the lists cannot be equal (they have a different number of nodes). Otherwise (if both lists have at least one node) we check the first values in these nodes for equality (since both lists are NOT null, both have first values), and if they are equal, we must also check for equality for the rest of the nodes in the lists; if they are not equal, the lists are not equal and we don't need to do any further computation There are many ways to code the check the null-ness of one list. For example, we can very explicitly write if (l1 == null && l2 == null) return true; if (l1 == null && l2 != null) return false; if (l1 != null && l2 == null) return false; which tests each of these three cases separately. It is equivalent (try all 3 cases) to the shorter (but less obvious) //Returns a value if either l1 or l2 is null if (l1 == null) return l2 == null; if (l2 == null) return false; //if got here, l1 != null Here is how I wrote this method (with one if for the 3 base cases) static boolean equals (LN l1, LN l2) { if (l1 == null || l2 == null) //if either is null, return true return l1 == null && l2 == null; // if and only if both are else return l1.value == l2.value && equals(l1.next,l2.next); } Notice because of the short-circuit property of of &&, if at any time l1.value == l2.value returns false, there will be no more recursion, because &&, when its first argument is false, doesn't evaluate its second argument -doesn't perform the recursive call (because whether it is true or false, the result of false && anything is false). The web reading shows a few recursive method that mutate a list: insertRear, InsertOrdered, removeFirst, and removeAll. What is striking is the similarity of all these three methods (they exhibit much more similarity than their iterative solutions). To start our understanding of such methods, let us first examine a method that returns a list that is a COPY of its parameter list (recall the elegant list-copying code written above), but with all nodes removed that store the value specified by the second parameter. The code is the same as the code above except if the first node stores value, it is not copied, but instead a copy is made of list following it (but also not including any nodes storing value). static LN copyRemove (LN l, int value) { if (l == null) return null; else if (l.value == value) return copyRemove(l.next,value); else return new LN(l.value, copyRemove(l.next,value)); } Try to work out, in English, the three proof rules. Note that this code, and the code below is called like y = copyRemove(x,5); that is, we don't call it as a void method, but as a method that returns a result: a reference to the copied list not containing any nodes storing 5 (x still refers to the first original list). If we want to make a copy of the list with certain values removed, we must assign the RESULT of the method to some variable refering to the new list. Think about what y = copyRemove (x,5); does for various lists, like a list with no values, or a list with one node storing a non-5 value, or a list with one node storing the value 5. The following similar code does no copying (no calls to "new LN"). It mutates the original list, removing from it the nodes storing value. static LN remove (LN l, int value) { if (l == null) return null; else if (l.value = value) return remove(l.next,value); else { l.next = remove(l.next,value); return l; } Notice the line in the original code return new LN(l.value, copyRemove(l.next,value)); which makes a copy of the first node, followed by a "copy of the rest of the list with no nodes storing value" is replaced by the two lines l.next = remove(l.next,value); return l; which keeps the first node in the returned list (by returning l, the reference to it) but first ensures that the all the nodes in the list after it (by storing into l.next) do not store "value" (i.e., those nodes are removed). I admit this is a bit subtle, but look at the SAME form l.next = something return l; in the context of the insertRear, insertOrdered, removeFirst, and removeAll methods. You might try doing a hand-simulation to help you understand the purpose of these lines of code.