Recursion

advertisement
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
Download