Recursion COMP 14, Fall 1998 Prasun Dewan1 8. Nested Whiles, Recursion, & Class Variables Reading: Section 5.14. Nested Whiles Supposed we want an extension of the problem of summing the first n numbers by allowing the users to enter a list of values for n that ends with a negative sentinel value. For each value of n entered by the user, the prints the sum, as shown below: In the above interaction, we need a loop to process the list of n’s entered by the user. In addition, for each value of n we need a loop to sum the first n numbers. So what we need is a nested loop: public static void sumTillSentinel() throws IOException { DataInputStream dataIn = new DataInputStream (System.in); int n = Integer.parseInt (dataIn.readLine()); while (n >= 0) { int sum = 0; int counter = 0; while (counter <= n){ sum = sum + counter; counter = counter + 1; } System.out.println("sum = " + sum); n = Integer.parseInt (dataIn.readLine()); } } Here we did a forward sum. We could have also done a backward sum: public static void sumTillSentinel2() throws IOException { DataInputStream dataIn = new DataInputStream (System.in); int n = Integer.parseInt (dataIn.readLine()); while (n >= 0) { int sum = 0; while (n > 0){ 1 Copyright Prasun Dewan, 1998. 1 Recursion sum = sum + n; n = n - 1; } System.out.println("sum = " + sum); n = Integer.parseInt (dataIn.readLine()); } } Though economical in how many variables it uses, this solution is potentially more dangerous, since both loops change the value of n. It actually works, since changes made to n by the outer loop do not depend on its previous value. Thus changes made to it by the inner loop to do not affect the outer one. In general, nested loops can be hard to read. Moreover, they are error-prone since two sets of counters or sentinel variables may be created and it is easy to give them the same names, which may cause them to interfere with each other. It is best to write a separate method for each loop, which makes the solution clearer, as shown below: public static void sumTillSentinel3() throws java.io.IOException { DataInputStream dataIn = new DataInputStream (System.in); int n = Integer.parseInt (dataIn.readLine()); while (n >= 0) { System.out.println ("sum " + sum(n)); n = Integer.parseInt (dataIn.readLine()); } } public static int sum (int n) { int sum = 0; while (n > 0) { sum = sum + n; n = n - 1; } return sum; } Here we don need to worry about the two changes to n interfering with each other since each method gets its own private n. Assignment Vs Equations Statements of the form: counter = counter + 1; sum = sum + n; in which a variable appears both on the right hand side and left hand side of = may seen unintuitive to you if you have been interpreting an assignment as a mathematical equation. As mathematical equations, we could never solve most of these statements. Consider: counter = counter + 1; This is equivalent to: 2 Recursion 0=1 which will never be true. Assignment does not solve a mathematical equation. It simply assigns the value computed by the rhs to the variable appearing on the lhs. The rhs must be computed before the assignment can take place. So, if the rhs refers to the lhs variable, it simply uses the old value of the lhs in the expression evaluation. Thus the statement: counter = counter + 1 is interpreted as: take the current value of counter, add to it 1, and store the sum as the new value of counter. Similarly, if Java was to execute the statements: int num; int numSquare = 25; numSquare = num * num; System.out.println(num); When it processes: numSquare = num * num; it does not solve for the uninitialized variable, num. In fact, the Java compiler will give you an error saying that an uninitialized variable is being used. Confusing assignment with equation evaluation is natural because of the unfortunate use of = for the assignment operator. It is for this reason that many languages use other symbols such as := and <-. Even if you were to use the equality operator, ==, Java will not solve an equation for you. Thus, to assert that int n is odd, some of you wrote: n == 2m + 1 where n and m are int variables. What you want to say is that n is odd if there exists an int m such that the above equality holds. Again, Java will not solve equations for you, it simply takes the current values of the variables used in the expression to determine their values. Recursion Loops are one mechanism for making a method execute a variable number of statements. Recursion offers an alternative mechanism, considered by many to be more elegant and intuitive. Recursive Sum To consider how it works, let us assume we need to write a function, sum, that takes as a positive integer parameter, n, and returns the sum of the first n positive integers. We saw earlier how to do this problem using whiles. We will now see the recursive solution to it. Before we look at the steps of the function, let us notice some properties of the function for different values of n. 3 Recursion sum (0) = 0 sum (1) = 1 = 1 + sum (0) sum (2) = 2 + 1 = 2 + sum (1) sum (3) = 3 + 2 + 1 = 3 + sum (2) which can be summarized as: sum (n) = 0 sum (n) = n + sum (n - 1) if n == 0 if n > 0 What we have above, in fact, is a precise definition of the problem we must solve. As it turns out it also gives us an algorithm for solving the problem: public static int sum (int n) { if (n == 0) return (0); if (n > 0) return n + sum(n-1); } If you were to try and compile this method, the compiler would complain, saying that the function does not return a value. This is because you have not covered all possible values of n. We are assuming here that n is a positive integer. Though that is the expected value, Java will not prevent a negative integer to be passed as a parameter, and we must specify what value should be returned in this case. Let us return 0 for negative values of n: public static int sum (int n) { if (n <= 0) return (0); else return n + sum(n-1); } A function such as sum that calls itself is called a recursive function. Notice how compact our recursive solution is. It can be considered more elegant than the while-loop solution because the algorithm for the problem is also the definition of the problem! It is easy to convince ourselves that our solution meets the requirements laid out by the definition, and we do need to risk the off-by-one errors of loops. The key to using recursion is to identify a definition that meets the following two requirements: 1. 2. Recursive Reduction Step: It defines the result of solving a larger problem in terms of the result of a smaller problem. As a result, it reduces the task of solving a problem into a smaller problem. In our example, the problem of computing the sum of n integers can be reduced to the problem of computing the sum of a smaller number, n-1, of integers. Base Terminating Case(s): It has a terminating condition indicating how the base case(s), that is, the smallest size problem(s), can be solved. In the example, it tells us that sum(0) = 0. In general, there can be more than one base case, for instance one for negative numbers and another for 0. Once we get the define the problem in this way, we can use recursion to solve it. The general form of a recursive algorithm is: if (base case 1 ) return solution for base case 1 else if (base case 2) return solution for base case 2 4 Recursion …. else if (base case n) return solution for base case n else do some preprocessing recurse on reduced problem(s) do some postprocessing Tracing a recursive call Before we look at other recursive problems, let us try to better understand sum by tracing its execution Assume the main method of ARecursionUser invokes: sum (2) When this call is made, we know the value of the formal parameter of the method, but not it’s return value. This situation can be expressed as: Invocation sum(2) n 2 return value ? Once the formal parameter is assigned, we can execute the if statement in the body of the execution. The if test fails, so the else part is executed, as shown by the window below: 5 Recursion The else part contains another call to sum, with the actual parameter 1. Thus, we now have two executions of the same method. We have seen before how the same method can be called multiple times, as in the following statements: System.out.println (nand(true, false) && nand (true, true)) In all the examples so far, one invocation of a method completed before another started. The situation is different here, since the second invocation is started before the first one ends. Thus, we have two copies of the method active concurrently, each with its own copy of the formal parameter and computing its own copy of the return value. The following table identifies the formal parameters and return values of the two invocations when the second call to sum is made: Invocation sum(1) sum(2) n 1 2 return value ? ? In a trace like this, it is usual to stack up the calls, that is, put an invoked method above the invoker. The topmost call with an uncomputed return value is the one the computer is currently executing. The following screen dump creates an alternative view of a call stack, showing in the pull down menu all the calls active at this point, including the call to main made by the interpreter: Let us now trace what happens as sum(1) executes its body. Again the test fails, and another call to sum is made, this time with the actual parameter 0: Invocation sum(0) sum(1) sum(2) n 0 1 2 return value ? ? ? When sum(0) executes the if, the test finally succeeds, and the function returns 0 and terminates execution. Invocation sum(0) sum(1) sum(2) n 0 1 2 return value 0 ? ? At this point control transfers back to sum(1), which adds the value returned by sum(0) to its copy of n , returns the sum, and terminates: 6 Recursion Invocation sum(0) sum(1) sum(2) n 0 1 2 return value 0 1 ? Control transfers now to sum(2), which adds the return value of sum(1) to its value of n, returns the sum, and terminates: Invocation sum(0) sum(1) sum(2) n 0 1 2 return value 0 1 3 This return value is received by main, which prints it, giving us the output: 3 Recursion Pitfalls You must be careful to have a terminating condition in a recursive program. Consider the following definition of sum which does not have a terminating condition: public static int sum (int n) { return n + sum(n-1); } Let us trace again the execution of: sum (2) This invocation computes 2 + sum (1) which in turn computes 1 + sum (0) which in turn computes 0 + sum (-1) which in turn computes -1 + sum (-2) which in turn computes -2 + sum (-3) and so on. So we get an infinite number of calls to sum. You must also be careful to make the recursive step compute a reduced version of the problem. Let us say we write the sum function as follows: 7 Recursion public static int sum (int n) { if (n <= 0) return (0); else return sum(n+1) – (n + 1); } Here, the recursive step, solves sum(n) in terms of a harder problem: sum (n+1). So if we call sum (2) it computes sum (3) - 3 which computes sum (4) - 4 and so on. So again, we get an infinite number of calls to sum again. Even though we have a terminating condition, we never reach it since we do not reduce the problem. It is easy to get an infinite number of calls. When this happens the computer runs out of memory space and gives the following error message saying there has been a stack overflow You must look carefully at the function to make sure you have a correct terminating condition and a reduced problem that eventually reaches that condition. The reason it runs out of memory space is that each time a function is called, space must be created for its arguments and its return value. Thus, a non-terminating sequence of recursively exhaust all of memory. The part of memory used to store formal parameters, variables and return values of methods is called the stack, since it stacks an invoked method’s data over the data of the calling method, as we showed in our tables above. Factorial Now that we understand how recursion works, let us consider some more recursive functions. Let us say we need to compute the factorial of some number n. We know that: factorial (0) = 1 factorial (1) = 1 = 1 * factorial (0) factorial (2) = 2*1 = 2 * factorial (1) factorial (3) = 3*2 = 3 * factorial (2) which can be generalized as: factorial (n) = 1 factorial (n) = n * factorial (n-1) if n <= 02 otherwise The recursive function follows easily: public static int factorial (int n) { if (n <= 0) 2 In all of the problems given here, we will compute a nonsensical value for the result if the argument is negative but we are expecting a positive number. 8 Recursion return (1); else return n * factorial(n-1); } Since factorial(0) is the same as factorial(1), we can make this method more efficient: public static int factorial (int n) { if (n <= 1) return (1); else return n * factorial(n-1); } Recursive Function with two parameters Both of the recursive examples took one parameter. Let us consider a recursive function with two parameters. Suppose we are to write a power function that raises a given base to some (positive) exponent. Thus: power (2,3) == 8 When we have two parameters, we need to know which parameter(s) to recurse on, that is, which parameters to reduce in the recursive call(s). Let us try first the base: power (0, 0) = 1 power (0, exponent) = 0 if exponent > 0 power (1, exponent) = 1 power (2, exponent) = 2*2* … 2 (exponent times) power (3, exponent) = 3*3* … 3 (exponent) There seems to be no relation between: power (n, exponent) and power(n-1, exponent) So let us try to recurse on the exponent: power (base, 0) = 1 power (base, 1) = base * 1 = base * power (base, 0) power (base, 2) = base * base = base * power (base, 1) which can be generalized to: power (base, exp) = 1 power (base, exp) = power (base, exp-1) if exp <= 0 otherwise Our recursive function, again, follows : public static int power (int base, int exp) { if (exp <= 0) return (1); else 9 Recursion return base * power(base, exp-1); } In this example, this two-parameter function recurses on only one of its parameters. It is possible to write recursive functions that recurse on both parameters, as you will see in your assignment. Recursive Procedures Like functions, procedures can be recursive. Consider the problem of saying “hello world” n times. We can write a recursive definition of this problem: greet(0): do nothing greet(1): print “hello world” = print “hello world”; greet(0); greet(2): print “hello world”; print “hello world” = print “hello world”; greet(1); So the general pattern is: greet(0): do nothing greet(n): print “hello world”; greet(n-1); Our recursive implementation follows: public static void greet (int n) { if (n > 0) { System.out.println("hello world"); greet(n-1); } } Recursive functions create recursive expressions, where a function typically evaluates one part of the expression and calls itself to evaluate the remainder. In contrast, a recursive procedure is an example of a recursive statement, where the procedure performs one step in a statement list, and calls itself recursively to perform the remaining steps. Recursive functions are perhaps more intuitive because, from our study of Mathematics, we are used to recursive expressions. However, both recursive functions and procedures are very useful and often lead to more succinct implementations than loops. Sentinel-based List Recursion As in the case of loops, we can use recursion for processing lists that are terminated by sentinels. Consider again the problem of adding a list of positive numbers entered by the user, where the list is terminated by a negative number. This time assume the numbers are doubles. Our recursive algorithm would be: addList () = 0 addList () = nextValue + addList() if (nextValue < 0) t otherwise Here the base step returns 0 because the list of remaining items is empty . The reduction step removes one item from a non-empty list, and adds it to the sum of the items in the remaining list. The recursive function, thus, is: public static double addList() { int nextValue = readDouble(); if (nextValue < 0) return 0; else return nextValue + addList(); 10 Recursion } Contrast this solution with the ones we have seen before. The previous cases were examples of number-based recursion: the base cases correspond to small values of one or numbers, and reducing the problem amounts to reducing the values of these numbers. This is an example of list-based recursion: the base case corresponds to small-sized lists, and reducing the problem involves reducing the size of the lists. We will see further examples of list-based recursion later. This solution is somewhat confusing because the list is not an explicit parameter of the function. Instead, it is implicitly formed by the series of values input by the user. Our solution assumes the existence of a function for reading the next double from the user. Class Variables Here is how we might implement this function: static DataInputStream dataIn = new DataInputStream(System.in); public static double readDouble() { try { return new Double (dataIn.readLine()).doubleValue(); } catch (IOException ioe) { System.out.println(ioe); return -1; // consider exception as list termination } } It puts reading of user input in a try block so that we do not have to declare a throws clause in the header of the function. A function is required to return a value for all execution paths through it. Therefore, if we execute the catch block in response to an exception, we must return a value. We regard an exception as list termination, and return a negative value in this case. Notice that the variable dataIn accessed by the function is not declared inside the function. Instead, it is declared in the class outside. It is our first example of a class variable. A class variable is a variable declared inside a class but outside all methods defined in the class. It’s declaration, like the declaration of a class method, must be preceded by the keyword static. A class variable can be shared by all methods declared in the class. This is not the case with variables declared inside methods, which we will refer to as method or local variables, and are private to the method. Like method arguments, different invocations of the same method get their own private copies of the local variables. We have created dataIn as a class variable rather than a local variable of readInt since we do not wish to create a new data input stream each time the function is called. Multi-Class Programs readDouble is such a useful function that we might want to put it in a separate class that can be used in multiple programs. In fact, we can consider creating a special class, called AKeyboard, that groups together all of the methods we define to read input: import java.io.IOException; import java.io.DataInputStream; class AKeyboard { static DataInputStream dataIn = new DataInputStream(System.in); public static double readDouble() { try { 11 Recursion return new Double (dataIn.readLine()).doubleValue(); } catch (IOException ioe) { System.out.println(ioe); return Double.NEGATIVE_INFINITY; // works as negative sentinel and error value } } // other reading functions such as readBoolean, readInt, readLine … } Since readDouble is now in a separate class, the function addList must use its full name when calling it: int nextValue = AKeyboard.readDouble(); Writing multi-class programs offers the same kinds of benefits as writing multi-method programs – it simply provides a larger-grain of modularity than what is provided by methods. In general we should: Keep related methods in the same class and unrelated methods in different classes. As a result, we can understand, design, browse, optimise each class independently. For instance, in this example, we can understand the class AKeyBoard without worrying about or even knowing all the classes that use it. Similarly, in the implementation of the class implementing addList, we do not have to worry about IOException, DataInputstream and other details of input. Library Classes AKeyboard is the first class we have seen that does not have a main method in it. Such as class cannot be executed by the interpreter. We will call such a class a pure library class since it must always used with some other class containing main. Classes with main can also be used as libraries in that the methods they declare can be called from other classes. For instance, ARecursionUser serves as a library to any class that calls its sum public method. However, since it does contain a main, it can be executed as a program, and thus is simply a library. Developing a Multiclass Program Developing a multiclass program requires more work. As mentioned before, you must create each class in a separate file. For now, put all library classes in the same directory as the main class of your program. 3 Be sure to compile a library class before it is used by another class. Thus, in the example above, if you were to use the command window, you would execute: javac AKeyboard.java javac ARecursionUser.java And to run the program, you simply need to name the main class: java ARecursionUser Java automatically finds all libraries to which it refers and links it with them dynamically, as the library method is called. If you have been creating projects, you need to add all the library files to your project. The build command will compile all project files in the right order. 3 A library class in another directory can be accessed by listing the directory in the CLASSPATH. 12 Recursion Pure and Impure Functions The function addList is not like the other functions, such as nand, we have seen so far. It is not a pure function in the mathematical sense of the word. The word “function” in mathematics denotes a computation whose only task is to determine the relationship between the operands and the result. If we call the function multiple times with the same arguments, it returns the same result. System.out.println (min (5,3)); // prints 3 System.out.println(min(5,3)); // prints 3 The min function we have looked at so far also has this property. The function addList does not have this property. It takes no argument, so all calls it to it are the same. But what it computes depends on the input System.out.println(addList()); // assume input is: 2 1 –1. returns 3 System.out.println(addList()); // assume input is: 3 1 –1. returns 4 Impure functions can be confusing. In a program that needs the result of an impure function multiple times, it matters whether you store the function result in a variable and later reuse the value, or simply call the function multiple times. Thus, the following program: Double addList = addList(); System.out.println (addList); System.out.println (addList); is not the same as: System.out.println (addList()); System.out.println (addList()); Therefore, you should try to write as few impure functions as possible. Side Effects Impure functions, often, have side effects. We normally think of the effect of a function as computing a return value. A “side effect” is any other effect of the function that can be observed after the function has terminated. Examples of side effects are: 1. 2. 3. Writing output to the transcript window. Advancing the read cursor in the transcript window by reading input. Changing a non-local variable, that is a variable (such as a class variable) shared with other methods in the class. Side effects cause the function to become impure, and therefore should be avoided as much as possible. Some side effects are more confusing than others: the three side effects above have been listed in the order of their potential for causing harm. Writing output is perhaps the most benign side effect, and is something we often need to do in a function while debugging it. In Java, reading input is also acceptable, since a method that returns a result based on processing the input stream of characters must be a function. 4 The most dangerous is the last one, and as it turns out, it is easily avoided without significantly reduce programming flexibility. Therefore, we will insist that you: Never change a non-local variable in a function.(Side Effect Principle.) Some programmers feel that even procedures should never change the values of non-local variables since it makes them less self-contained. However, the whole concept of object-oriented programming, as we shall see later, relies on procedures changing non-local variables, and its benefits far outweigh the drawbacks. 4 In some languages such as Pascal, it can be a procedure that returns results through its parameters. 13 Recursion Not all impure functions have side effects. For instance, the function getTime()does not change the time but does return different values each time it is called. In general, a function is impure if the state used to compute its result is changed between multiple invocations it. In the case of impure functions with side-effects, the changes are made by the function itself. In the case of impure functions without side-effects, the changes are made by the external environment. 14