Lecture 24 (Tychonievich) - Recursion

Lecture Date: Monday, March 16

We said earlier that if you have a method like

Bad (initite) Recursion
1
2
3
public static int bad(int x) {
    return bad(x);
}

that Java will run and run until it runs out of memory and never make progress. This is because when we call something like bad(3), Java does the following:

  • The code for bad(3) is return bad(x);, so first I have to evaluate
    • the code for bad(3) is return bad(x);, so first I have to evaluate
      • the code for bad(3) is return bad(x);, so first I have to evaluate
        • the code for bad(3) is return bad(x);, so first I have to evaluate
          • the code for bad(3) is return bad(x);, so first I have to evaluate
            • ….

This is called infinite recursion.

However, not all recursion is infinite. In particular, methods can invoke themselves provided they satisfy the following three properties:

  1. They have a recursive case – for some inputs they invoke themselves.
  2. They have a base case – for some inputs they do not invoke themselves.
  3. The recursive case makes progress toward the base case in some way, usually by changing the arguments.

For example,

Good (meaning not-infinite) Recursion
1
2
3
4
5
6
7
public static int good(int x) {
    if (x < 0) {
        return -x;
    }
    int recVal = good(x - 1);
    return recVal + 2;
}

will be evaluates as follows:

This is because when we call something like bad(3), Java does the following:

  • The code for good(3), since 3 is not < 0, requires that I first evaluate good(2)
    • the code for good(2), since 2 is not < 0, requires that I first evaluate good(1)
      • the code for good(1), since 1 is not < 0, requires that I first evaluate good(0)
        • the code for good(0), since 0 is not < 0, requires that I first evaluate good(-1)
          • the code for good(-1), since -1 is < 0, returns a 1
        • good(-1) returned a 1, and then I add 2 to that, returning a 3
      • good(0) returned a 3, and then I add 2 to that, returning a 5
    • good(1) returned a 5, and then I add 2 to that, returning a 7
  • good(2) returned a 7, and then I add 2 to that, returning a 9

Note that just like infinite recursion we start by good invoking good invoking good… but each new invocation makes progress, eventually reaching the base case. After the base case we have to walk our way back out one invocation at a time.

I class today we’ll explore various ways of evaluating recursive code.

Examples

Short and complicated
1
2
3
4
5
6
7
8
9
10
11
public class Puzzle {
    public static int count;
    public static int f(int x) {
        Puzzle.count += 1;
        if (x % 5 == 0) {
            return x;
        } else {
            return Puzzle.f(x + 7) + Puzzle.f(x - 3);
        }
    }
}
With an array
1
2
3
4
5
6
7
8
9
public static int f(int[] x, int v, int i) {
    if (i >= x.length) {
        return -1;
    }
    if (x[i] == v) {
        return i;
    }
    return f(x, v, i+1);
}

You can find more and even more at codingbat.com.

From Lecture

Q: Why is it bad to invoke method within itself?
A: It’s only bad if you don’t have a base case and make progress towards it; it is bad because infinite recursion causes Java to crash.

Q: When is recursion better than loops?
A: Roughly speaking, when you need 2+ recursive calls per call.

Q: Any reason to declare a method that does nothing?
A: Basically not, except as a midway point toward writing a larger program.

Q: What about count in Puzzle.java?
A: It’s a static field; useful when you need to share variables between methods (but discouraged; it tends to make hard-to-read and buggy code more likely).

Q: When a non-main method vs main?
A: Any time you can name the action you are doing. Breaking up code into methods makes it easier to understand and less likely to have bugs.