Lecture 6: Recursion

advertisement
Recursion
Mathematically, a recursive function is one that is defined in
terms of other values of that same function. A couple
(overused) examples are as follows:
Fibonacci numbers:
F(0)=0, F(1)=1, F(n) = F(n-1)+F(n-2), for all n>1. (F is defined
for all non-negative integers here.)
Factorials:
Fact(0) = 1, Fact(n)=n*Fact(n-1), for all n>0. (Fact is also
defined for all non-negative integers.)
Analogously, a recursive method is one where the method body
contains a call to the method itself. (Remember, each instance a
method is called, the computer allocates some memory for
THAT instance to run. There is no reason why two
DIFFERENT instances of the same method can not be in the
middle of running at the same time.) You have seen recursion
in CS1. Probably most of the recursion you saw in that class
was straightforward, such as implementing the two functions
listed above. In this class we will look at more complicated
recursive algorithms.
Incidentally, here are Java methods that compute the two
recursive functions listed above:
public static int Fib(int n) {
if (n<2)
return n;
else
return Fib(n-1)+Fib(n-2);
}
public static int Fact(int n) {
if (n ==0)
return 1;
else
return n*Fact(n-1);
{
What would make both of these methods more robust?
What problem becomes more probable with recursion?
When dealing with recursion, there are essentially two issues to
tackle:
1) How to trace through recursive code.
2) How to write recursive code.
Tracing through recursion
Clearly, the first is easier than the second, and you can't do the
second if you can't do the first. So, let's talk about tracing
through recursion first.
Really, there are no NEW rules to use when tracing through
recursion, as compared to tracing through code that has
method calls. Anytime a new method is called, you checkpoint
and STOP running the calling method, and start executing the
callee method. When the callee has terminated, it returns a
value to the caller, and then the caller continues execution
EXACTLY where it left off.
The stack trace illustration your CS1 book uses is a fairly good
model to use when tracing recursion. I often explain a trace
using a physical stack of papers, where each paper is allowed
to keep track of one method call. Let's trace through a couple
simple examples of the two functions above.
Writing recursion
The toughest part with writing a recusive method ISN'T
coding the method. Rather, the most difficult part is coming up
with the recursive algorithm to solve the problem. Once you
come up with the idea for the algorithm, the actual code is not
so difficult to write. (As you can see, the mathematical
definition of both fibonacci and factorial look amazingly
similar to the code. Once you can get this mathematical
definition, the code is not far off.)
The most difficult part of creating a recursive algorithm is
finding a way to solve the given problem that involves a
solution to a problem of the exact same nature. In both the
Fibonacci and Factorial examples, the functions themeselves
are defined recursively, so we don't have that problem. Here is
a problem you were probably shown in CS1 that is not
typically defined recursively:
Find the sum 1+2+3+...+n.
You probably did a for loop in a function to find this sum in
CS1. Then, in order to come up with a recursive solution, you
first had to think of a mathematical way to express the
function above, recursively.
The big key is that 1+2+3+... = n + (1+2+3...+n-1)
Thus, if we let s(n)=1+2+3+...+n, then we can derive that
s(n) = n + s(n-1), for all n>1, s(1)=1.
Now using the information above, the recursive method that
computes the function s above is apparent.
Once you have found a recursive algorithm to solve a problem,
there is one other main issue to consider before coding a
recursive method: For what values should the recursive
algorithm be executed, and for what values should it NOT be
executed, because the value to return can be easily computed?
This is known as the terminating condition. All recursive
methods have to have a terminating condition in some shape or
form. If they didn't, you would get infinite recursion. (This is
very similar to an infinite loop.)
There are two basic constructs that most recursive methods
take. Here they are:
if (terminating condition)
finish task
else
solve problem recursively
if (not terminating condition)
solve problem recursively
One other way to view coding a recursive problem is the
following:
Imagine that you have been asked to write a method, but that
someone else has already written the method and you are
allowed to make as many calls to THAT method that you want
to, as long as you don't pass it the same parameters that have
been passed to you.
Thus, when computing Fact(n), for example, instead of doing
all that multiplying yourself, you call the function your friend
has written to calculate Fact(n-1). When he/she returns that
answer to you, all you have to do to finish your job is multiply
that answer by n and you are done!!!
Recursive Binary Search
Here the key is that we have two terminating conditions:
having no search space, and if the middle element is the target.
After that, based on our comparison with the middle element,
we can make one of two recursive calls for a search in either
the lower or upper half of the array.
public static boolean search(int [] vals, int low, int high, int t) {
if (low > high)
return false;
int mid = (low+high)/2;
if (vals[mid] == t)
return true;
else if (vals[mid] < t)
return search(vals, mid+1, high, t);
else
return search(vals, low, mid-1, t);
}
Minesweeper - Recursive Clear
Minesweeper is the popular game included with windows
where the player has to determine the location of hidden
bombs in a rectangular grid. Initially, each square on the grid
is uncovered. When you uncover a square, one of three things
can happen:
1) A bomb is at that square and you blow up and lose.
2) A number appears, signifying the number of bombs in
adjacent squares.
3) The square is empty, signifying that there are no adjacent
bombs.
In the real minesweeper, when step 3 occurs, all of the adjacent
squares are automatically cleared for you, since it's obviously
safe to do so. And then, if any of those are clear, all of those
adjacent squares are cleared, etc.
Step 3 is recursive, since you apply the same set of steps to
clear each adjacent square to a square with a "0" in it.
I am going to simplify the code for this a bit so that we can
focus on the recursive part of it. (I will replace some code with
comments that simply signify what should be done in that
portion of code.)
The key here is ONLY if we clear a square and find 0 bombs
adjacent to it do we make a recursive call. Furthermore, we
make SEVERAL recursive calls, potentially up to 8 of them.
public boolean domove(int row, int col) {
// Take care of losing move
if (realboard[row][col] == "*") {
...
}
// Take care of case where you've already uncovered that sq.
else if (board[row][col] != '_') {
...
}
// Normal case
else {
// Finds # of adjacent bombs.
int num = numberbombs(row,col);
totalmoves--; //keeps track of total moves left to win.
board[row][col] = (char)(num+48); //Changes board
// to indicate move
// Handles recursive case
if (num == 0) {
for (int i=-1; i < 2; i++) {
for (int j=-1; j < 2; j++) {
if (valid(row+i,col+j) &&
board[row+i][col+j]=='_')
domove(row+i,col+j);
}
}
}
return false;
}
}
Introduction to Towers of Hanoi
The story goes as follows: Some guy has this daunting task of
moving this set of golden disks from one pole to another pole.
There are three poles total and he can only move a single disk
at a time. Furthermore, he can not place a larger disk on top of
a smaller disk. Our guy, (some monk or something), has 64
disks to transfer. After playing this game for a while, you
realize that he's got quite a task. In fact, he will have to make
264 - 1 moves total, at least. (I have no idea what this number is,
but it's pretty big...)
Although this won't directly help you code, it is instructive to
determine the smallest number of moves possible to move these
disks. First we notice the following:
It takes one move to move a tower of one disk.
For larger towers, one way we can solve the problem is as
follows:
1) Move the subtower of n-1 disks from pole 1 to pole 3.
2) Move the bottom disk to pole 2.
3) Move the subtower of n-1 disks from pole 3 to pole 2.
We can now use this method of solution to write a method that
will print out all the moves necessary to transfer the contents
of one pole to another. Here is the prototype for our method:
public static void towers(int n, int start, int end);
n is the number of disks being moved, start is the number of
the pole the disks start on, and end is the number of the pole
that the disks will get moved to. The poles are numbered 1 to 3.
Here is the method:
public static void towers(int n, int start, int end) {
if (n > 0) {
int mid = 6 - start - end;
towers(n-1, start, mid);
System.out.print("Move disk "+n+" from tower ");
System.out.println(start+" to tower "+end+".");
towers(n-1,mid,end);
}
}
Recursive Method to Print Out a Number in Any Base
Given a decimal number, consider printing it out in another
base. For the ease implementation, let's assume the base is 10
or less. (In the text they show a small work around that works
for bases up to 16, that could be extended to even larger bases.)
The basic idea is as follows:
Given a decimal number D in base B, we can determine the
following:
1) The least significant digit
2) The value of the leftover number
1) This is simply D%B.
2) What is leftover can be represented as D/B, using integer
division.
Thus, if we are given a number with more than one digit in
base B, we solve the problem recursively as follows:
1) Print out the whole number, except for the last digit.
2) Print out the last digit.
Note that #1 is the recursive part of the algorithm. In code, we
have:
public static void printInt(int n, int base)
{
if (n >= base)
printInt(n/base, base);
System.out.print(n%base);
}
Modular Exponentiation
The modular exponentiating problem is calculating xn mod p.
Note that the answer will always be in between 0 and p-1,
inclusive. Here is the standard iterative solution:
public static int modexp(int x, int n, int
p) {
int ans = 1;
for (int i=0; i<n; i++)
ans = (ans*x)%p;
return ans;
}
This looks like a perfectly acceptable solution, until you realize
that in practice, instead of using ints, we may use BigInteger
objects and that n could be as long as 50 or 60 digits.
Here is one recursive solution to this problem:
public static int modexp(int x, int n, int
p) {
if (n == 0)
return 1;
else if (n == 1)
return x%p;
else
return (x*modexp(x,n-1,p))%p;
}
Is this any better than the iterative method above it?
It is not. The reason is that each recursive call will spawn one
more, and the chain or recursive calls is n calls long, just as our
iterative loop runs n times. The problem is that n could simply
be too big for this to be feasible.
Now consider the following idea:
If n is even, then we could instead calculate xn/2 mod p, and
then square this one answer.
If n is odd, do the usual recursion from above.
Why is this idea a good one?
Because, once we determine xn/2 mod p we are SAVING
ourselves the work of recalculating it to perform the squaring
operation!!!
Here's the code:
public static int modexp(int x, int n, int
p) {
if (n == 0)
return 1;
else if (n == 1)
return x%p;
else if (n%2 == 0) {
int temp = modexp(x,n/2,p);
return temp*temp%p;
}
else
return x*modexp(x,n-1,p)%p
}
Now, we must assess the running time of this method.
The first two methods ran in O(n) time, where n is the value of
the exponent. (This assumes that multiplications take constant
time, an assumption that isn't necessarily the case.)
Notice that if n is even, we make one multiplication and one
recursive call with an exponent of n/2. Thus, if we let T(n) be
the running time of the algorithm, if n were even, we find:
T(n) = T(n/2) + O(1), assuming a constant time multiply.
BUT, there's a problem when n is odd! In this case,
T(n) = T(n-1) + O(1).
But wait! If n is odd, THEN n-1 is even. So, when n is odd, we
really see that we do two multiplications and one recursive call
with the exponent (n-1)/2, if we carry out the recursion one
extra step. (Alternatively, we could have written the code to
omit this extra recursive call.) So, really, if we assume that n/2
stands for an integer division, T(n) always satisfies:
T(n) = T(n/2) + O(1), for both n being even AND odd.
Now, let's solve this recurrence relation:
T(n) = T(n/2) + O(1)
= T(n/4) + O(1) + O(1)
= T(n/8) + O(1) + O(1) + O(1)
...
= T(n/2k) + O(1) + ... + O(1)
-------------------- (O(1) appears k times)
= kO(1), where n = 2k. Thus, k = log n. So, T(n) = O(log n)
As you can see, this running time is far superior to that of the
original two methods shown.
Another way to look at this analysis is that the exponent gets
divided by two for each two "iterations" of the recursion, so
using the repeated halving rule, it must get down to 1 in 2logn
iterations, where n is the exponent.
Another way to solve the recurrence relation above:
T(n) = T(n/2) + O(1)
T(n/2) = T(n/4) + O(1)
T(n/4) = T(n/8) + O(1)
...
T(2) = T(1) + O(1)
----------------------------- (Add all of these)
RHS = T(n) + T(n/2) + ... + T(2)
LHS = T(n/2) + ... + T(2) + T(1) + kO(1), where k = log n.
These two sides are equal to one another. Set them equal and
cancel out common terms on both sides, yielding:
T(n) = T(1) + kO(1) = O(k), since T(1) = O(1).
One final question:
If we use the same construct for regular exponentiation
(without modding), we find that the running time of THIS
algorithm is hardly different than the original iterative one.
BUT, when we mod, this algorithm is far, far quicker.
WHY IS THAT?
Euclid's Algorithm
This algorithm is often described iteratively, but can just as
easily be described recursively. Consider taking the gcd of 138
and 36. Our first step is:
138 = 3*36 + 30
In essence we divide 36 into 138 to yield a remainder of 30.
Next, we calculate
36 = 1*30 + 6
Here we see that we just repeat the algorithm. Namely, the
gcd(138, 36) = gcd(36, 30).
Using this idea, we can find the gcd of two positive integers a
and b as follows:
public static int gcd(int a, int b) {
if (b == 0)
return a;
else
return gcd(b, a%b);
}
Why does this work, even when a is less than b?
Recursive Problem to Solve
Write a recursive method to determine the number of 1s in the
binary representation of a positive integer n. Here is the
prototype:
// Precondition: n > 0.
public static int numOnes(int n);
Download