21: Introduction to Recursion

Announcements

I’m going to drop your lowest quiz grade.

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

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 optional else 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.

  1. Does it have a base case?
  2. Does every recursive call make progress toward (or arrive at) the base case?
  3. 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 (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).