22: Introduction to Recursion
Announcements
A11 extended 24 hours.
Quiz Monday, don’t forget!
Quiz policy.
Assignment policy.
Organizing your War code
Do what you like, of course, but I suggest something like the following pseudocode:
findWinner():
instantiate a War object
War:
constructor:
initialize decks, other values
simulateGame():
while !gameOver:
battle()
return outcome
battle():
if game over (winner or draw):
set outcome
return
increment battle count
take card from each player, add to spoils
if a player wins:
allocate spoils
else war()
war():
if game over (not enough cards for war):
set outcome
return
remove three cards from each player, add to to spoils
Q+A session on War
(No notes, just whatever comes up in class.)
Recursion
In common language, a “circular definition” is not useful. When you define something in terms of itself, it conveys no meaningful information: “Good music is music that’s good.” tells you nothing.
But math (and computer science) often uses recursive definition to define concepts in terms of themselves. WTF?
For example:
- directories contain files and/or directories
- an if statement in Java is made of
if
, an expression that evaluates to a boolean, and a statement (plus an optionalelse
and another statement). - a postfix expression is either an operand or two postfix expressions followed by an operator
and so on.
How do the above differ from our bad definition of good music? While they are defined in terms of themselves, the circularity is only partial – there’s a way out of the circle. (show on board)
Recursive algorithms: an example
It turns out some algorithms are most naturally expressed recursively – usually they correspond to solving problems or implementing abstractions that are also defined recursively. A big chunk of COMPSCI 250 explores the relationship between recursion and inductive proofs.
For example, consider the factorial function over non-negative integers. n-factorial (usually written n!) is the product of the numbers 1 through n: (1 * 2 * 3 * … * n)
4! = 1 * 2 * 3 * 4 = 24.
What is 0!? 1. Why? The intuition is that you’re multiplying together “no numbers”, which is like not multiplying at all. What’s the same as not multiplying at all? Multiplying by 1. The real reason for this has to do with the algebra of integers (which differs from the algebra you learned in middle/high school); take an abstract algebra or number theory course if you want to learn more.
We can recursively define the factorial function.
0! = 1
n! = n * (n-1)!
If we use this definition to compute 4!, we get 4 * 3!. Applied again, we get 4 * 3 * 2!, finally, we get 4 * 3 * 2 * 1, which is the same as the non-recursive definition.
Both are relatively easy to transcribe into Java:
static int factorial(int n) {
int x = 1;
for (int i = 1; i <= n; i++) {
x *= i;
}
return x;
}
static int factorial(int n) }
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
What actually happens when we call the recursive factorial
with an argument of 4?
That call makes a call to factorial(3)
, which calls factorial(2)
, which calls factorial(1)
, which calls factorial(0)
, which returns 1 to factorial(1)
, which returns 1 to factorial(2)
, which returns 2 to factorial(3)
, which return 6 to factorial(4)
, which returns 24, which is the right answer.
The values are passed along on the call stack, which grows to a height of five while this recursive method operates. Why doesn’t it just blow up? Because eventually the recursion terminates. How do we know it will terminate, and how do we know the answer is right? Glad you asked.
Three questions (about recursion)
Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see.
There are three questions we must answer affirmatively about a recursive algorithm if we expect it to (a) terminate and (b) produce the correct answer.
- Does it have a base case?
- Does every recursive call make progress toward (or arrive at) the base case?
- Does the algorithm get the right answer if we assume the recursive calls get the right answer?
A base case is the end of the line, when there is no more recursion. factorial
‘s base case is when n=0.
Guaranteeing progress is trickier. If our parameter is numerical, and each recursive call brings us a constant amount closer toward the base case (as is the case of factorial
) then we can be certain we’ll eventually get there. But sometimes you need two recursive calls to make progress (see foo
below); this condition is a little more loose.
Justifying correctness is something you’ll do more in COMPSCI 250, but in short, if you are proceeding from a correct recursive definition, your recursive algorithm will be correct.
Another example:
void clear(Stack<T> s) {
if (!s.isEmpty()) {
s.pop();
clear(s);
}
}
What’s the base case?
And another:
int foo(int x) {
if (x <= 1) {
return 0;
}
else if (x % 2 == 1) {
return x + foo(x + 1);
}
else { // x % 2 == 0
return x + foo(x / 2)
}
}
Does it make progress each step? No. But at least every other step it does – good enough, since the progress it makes (x / 2
) strictly dominates the anti-progress (x + 1
).
In class exercise
What’s the traditional way find the sum of integers between 0 and n?
public static int sumNIterative(int n) {
if (n < 0) {
throw new IllegalArgumentException();
}
int sum = 0;
for (int i = 0; i <= n; i++) {
sum += n;
}
return sum;
}
Rewrite it recursively:
public static int sumN(int n) {
if (n == 0) {
return 0;
} else if (n > 0) {
return n + sumN(n - 1);
} else { // n < 0
throw new IllegalArgumentException();
}
}
And again:
public static int biPowerIterative(int n) {
if (n < 0) {
throw new IllegalArgumentException();
}
int result = 1;
for (int i = 0; i < n; i++) {
result *= 2;
}
return result;
}
Recursively:
public static int biPower(int n) {
if (n == 0) {
return 1;
} else if (n > 0) {
return 2 * biPower(n - 1);
} else { // n < 0
throw new IllegalArgumentException();
}
}