21: Introduction to Recursion
Announcements
Almost there! 2.5 weeks to go!
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 or technical 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
Notice that we did not use recursion in War, even though we’re learning about it in class! You can certainly implement aspects of it recursively, I don’t recommend it, nor will I help you debug a crazy three-way recursion among different methods.
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.
The non-recursive definition is relatively easy to transcribe into Java (we’ll not handle invalid inputs, like -1):
static int factorial(int n) {
int x = 1;
for (int i = 1; i <= n; i++) {
x *= i;
}
return x;
}
What about the recursive one? Baby steps. Let’s define factorial0
, then factorial1
, and so on:
static int factorial0() }
return 1;
}
static int factorial1() }
return 1 * factorial0();
}
static int factorial2() }
return 2 * factorial1();
}
static int factorial3() }
return 3 * factorial2();
}
static int factorial4() }
return 4 * factorial3();
}
factorial4
calls factorial3
; once factorial3
returns an answer, factorial4
returns its answer. How does factorial3
work? The same way, using factorial2
. In fact, everything except factorial0
works the same way. So why not capture this pattern in a single method:
static int factorial(int n) }
return n * factorial(n - 1);
}
That’s not quite right, because it won’t “stop” when it gets to zero. We could check for the zero case and call factorial0
:
static int factorial(int n) }
if (n == 0) {
return factorial0();
} else {
return n * factorial(n - 1);
}
}
or just get rid of it entirely:
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, which is the same as what happened when we had different (factorial1
, factorial2
, etc.) methods. But we do it all from one method!
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 (more generally: every bounded sequence of recursive calls) 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).
Justifying correctness is something you’ll do more in COMPSCI 250, but in short, if you are proceeding from a correct recursive definition (which factorial is) then your recursive algorithm will be correct.
In-class exercise
int sumToN(int n) {
if (n == 1)
return 1;
else
return n + sumToN(n - 1);
}
What’s the base case?
On valid inputs (n > 0
), does this method make progress toward the base case?
Another example:
void clear(Stack<T> s) {
if (!s.isEmpty()) {
s.pop();
clear(s);
}
}
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
).