CSE143 Notes for Wednesday, 2/1/06

We continued our discussion of recursion by looking at several more examples. I didn't have time on Monday to do the problem that involved binary numbers. I pointed out that when you count in binary, you only have two digits to work with. So in base 10, you don't run out of digits and switch to 10 for a while (0, 1, 2, ..., up to 9). But in base 2, you run out of digits almost right away (0, 1, out of digits). So in base 2, counting goes like this:

        base 10    base 2
        -----------------
         0          0
         1          1
         2          10
         3          11
         4          100
         5          101
         6          110
         7          111
         8          1000
I said that I didn't have time to go through this example in detail, but that I'd explain the basic idea. It's a nice application of recursion, because we've seen that recursion just requires you to be able to do a small bit of the overall problem, to nibble away at the problem. So I said consider a number like 345,927. What is its base-2 representation? Nobody in the class was geeky enough to know the answer (or at least they weren't willing to let people know that they were that geeky). But I asked people if they could tell me anything about the base-2 representation of 345,927. Someone said it ends in 1. How do you know? Because it's an odd number. Even numbers end in 0 in base-2 and odd numbers end in 1.

This doesn't seem like much, but that small observation is enough to solve the overall task. It requires some insight into the problem that I didn't have time to develop in detail, but I made an analogy. I reminded people that when we worked on stutter and were considering a base-10 number like 342, we used division by 10 to divide it into two pieces:

        n = 342

             34 | 2
         -------+-------
         n / 10 | n % 10
It turns out that the same kind of trick works in base-2. Division by 10 gives you the leading digits in base-10 versus the final digit in base-10. The corresponding rule in base-2 would be:

n = 345,927 <leading base-2 digits of 345,927> | 1 -----------------------------------+------ n / 2 | n % 2 So to write out the base-2 representation of n, we simply write the base-2 representation of (n / 2) and then write the final digit (n % 2). The base case is for numbers that are the same in base-2 and base-10 (0 and 1). Throwing in a case for negatives, we end up with the solution from handout #14:

        public static void writeBinary(int n) {
            if (n < 0) {
                System.out.print("-");
                writeBinary(-n);
            } else if (n < 2)
                System.out.print(n);
            else {
                writeBinary(n/2);
                System.out.print(n % 2);
            }
        }
This relatively short and simple method will write out the binary representation of any number. And if you change the three occurrences of "2" to "3", then it writes out the base-3 representation of a number. If you change the 2's to 4's, it writes out the base-4 representation. And so on.

Then I said that I'd like to write a recursive version of integer exponentiation. This is referred to as "pow" in the Math class (short for "power"). In our case, we are limiting ourselves to integers as parameters and an integer return type:

        // post: returns x to the y power
        public int pow(int x, int y) {
            ...
        }
I asked people whether there were any values of y that might cause a problem. Someone mentioned that negative values of y would be problematic. That's because we've specified our return type as int. If you carry an int to a negative exponent, you get something that is not an int. So we added a precondition to the method:

        // pre : y >= 0
        // post: returns x to the y power
        public int pow(int x, int y) {
            ...
        }
In fact, this is a good case to begin our code with a test to see if the precondition has failed. If so, then we throw an exception:

        // pre : y >= 0
        // post: returns x to the y power
        public int pow(int x, int y) {
            if (y < 0)
                throw new IllegalArgumentException("negative power");
            ...
        }
This kind of testing of preconditions fits in nicely with a typical recursive definition because it is usually written in terms of a series of cases. Next I asked people about the base case for this method. What is the simplest power to compute? Someone said that y equal to 1 is simple and it is, but someone else mentioned that y equal to 0 is even simpler. In that case, the result is 1:

        // pre : y >= 0
        // post: returns x to the y power
        public int pow(int x, int y) {
            if (y < 0)
                throw new IllegalArgumentException("negative power");
            else if (y == 0)
                return 1;
            ...
        }
And what about the recursive case? We get this by noticing that the following is true:

xn = x * xn - 1
This can easily be turned into a recursive definition:

        // pre : y >= 0
        // post: returns x to the y power
        public int pow(int x, int y) {
            if (y < 0)
                throw new IllegalArgumentException("negative power");
            else if (y == 0)
                return 1;
            else
                return x * pow(x, y - 1);
        }
And that completes the method. That's all we have to do. Of course, it wouldn't have been too difficult to write this iteratively either, but it's nice to see that it can be easily written recursively.

Then I said that I wanted to consider a variation that makes this a little more interesting. So I asked people whether if they were computing something like 264, whether they would really use their calculator to say 2 * 2 * 2 * 2 * 2 * 2, and so on, 64 different times. Isn't there a better way? Someone mentioned that we could compute 232 and square it. That would lead to this formula:

xn = (xn/2)2
I mentioned that it's a little more convenient for our purposes to use this slight variation:

xn = (x2)n/2
I asked under what circumstances this equation would be true if we were dealing with values of type int and truncated integer division. Someone mentioned it would be true for even values of n. This allows us to write a variation of the pow method that includes this case:

        if (y % 2 == 0)
            return pow(x * x, y / 2);
We want to be careful not to do this test before our base case, because 0 is an even number. And we had to think about what to do if we have a value of y that isn't an even number. The easy thing there is to keep our old line of code from the original pow. So this led to a new version of the method:

        // pre : y >= 0
        // post: returns x to the y power (this version is O(log y))
        public int pow2(int x, int y) {
            if (y < 0)
                throw new IllegalArgumentException("negative power");
            else if (y == 0)
                return 1;
            else if (y % 2 == 0)
                return pow2(x * x, y / 2);
            else
                return x * pow2(x, y - 1);
        }
This version will find an answer much faster than the other version. Dividing y by 2 gets you closer to 0 much faster than subtracting one from y does. I asked people how often we'd be dividing by 2 versus subtracting 1. It depends a lot on the exponent we're computing, but one of the nice things about odd numbers is that if y is odd, then (y - 1) is even. So this version of the method uses the "even y" branch at least every other call.

How many times would you have to divide n in half to get down to 0? That, by definition, is the log to the base 2 of n. So if you were computing something that involved an exponent of a billion (109), the first version of pow would make a billion calls, but this one would make at most around 60 calls because the log of a billion is around 30 and this method does the even case (dividing y in half) at least every other call. It's much better to have 60 calculations to perform than a billion. In terms of big-Oh, the first algorithm is O(n) where n is the power and the second is O(log n).

I also pointed out that it probably seems artificial to think about computing really high powers of y because type int is limited in how many digits it can store. That's true, so for this particular version it might not matter that much. But Java has a type called BigInteger that can store an arbitrary number of digits and if you study the kinds of techniques used in areas like encryption, you'll find that it is very common to carry large integers to the power of other large integers. In a case like that, it could make a huge difference to use the second approach instead of the first.

I didn't have much time left, so I quickly discussed the last two examples that I had. They come from the field of combinatorics and are known as "combinations" and "permutations". Many calculators have function keys for computing these values (usually listed as C(n, m) or P(n, m)). They also exist in Excel and are called "combin" and "permut". I decided to use the Excel function names.

The "permut" function is easier, so I did that one first. The idea is to count how many sequences you can form using "m" items chosen from a pool of "n" items (what is written as P(n, m) or permut(n, m)). For example, suppose you have a deck of 52 cards and you want to know how many sequences of 3 cards you can form. Here n is 52 and m is 3 (we are picking 3 cards from a deck of 52). In this case we are assuming that order matters (first card versus second card versus third card). How many ways are there to do it? There are 52 choices for the first card, 51 choices for the second (because you will already have picked one for the first slot and you'll have 51 left) and 50 choices for the third card. So there are 52 * 51 * 50 ways to do this.

I asked people to think about how we might write this recursively. What is an easy value of m to work with? Again people mentioned an m of 1 and that is pretty easy, but even easier is an m of 0. If you're asked how many ways there are to pick 0 items, the answer is that there is exactly 1 way to pick 0 items from n. And what if you've been asked to pick m items from n? There are n ways to pick the first and what is left involves picking (m - 1) items from the (n - 1) that are left. In other words:

        permut(n, m) = n * permut(n - 1, m - 1)
Putting these two facts together and including some reasonable preconditions for m and n, we get the following solution:

        // pre : n >= 0, 0 <= m <= n
        // post: returns the number of m-permutations of n items (number of
        //       ways to order m items chosen from n)
        public int permut(int n, int m) {
            if (n < 0 || m < 0 || m > n)
                throw new IllegalArgumentException();
            else if (m == 0)
                return 1;
            else
                return n * permut(n - 1, m - 1);
        }
I didn't have time to do the combinations version in detail, but I mentioned the basic ideas. When you are computing combinations, we assume that order doesn't matter. With poker hands, for example, you just care what the cards are, not the order in which they appear. There is a simple rule you can develop for this that involves thinking about one of the items that may or may not be chosen. I said to consider the idea that we have a pool of n people from which we are choosing m prize winners. They all win the same prize, so order doesn't matter. We just need to know who the m prize winners are from among the n people in the pool.

I said to consider the fate of one particular person. Call him Joe. There are two possibilities and we have to include both of them. Joe might be a winner or he might not be a winner. Both cases are possible, so we should add together the number of cases where Joe is a winner to the number of cases where Joe is a loser:

        total cases = (cases where Joe wins) + (cases where Joe loses)
So think about the case where Joe wins. We would have (m - 1) winners left to choose (because Joe is one of them) and we'd have (n - 1) people left to choose from (because we've already considered Joe). And what about the case where Joe loses? Then we'd have all m winners left to choose and we'd have (n - 1) people to choose from.

This doesn't seem like much, but it turns out to be the basis of our recursive case:

        combin(n, m) = combin(n - 1, m - 1) + combin(n - 1, m)
That along with some appropriate base cases and some reasonable preconditions allows us to write the following method:

        // pre : n >= 0, 0 <= m <= n
        // post: returns "n choose m" (number of ways to choose m items from n)
        public int combin(int n, int m) {
            if (n < 0 || m < 0 || m > n)
                throw new IllegalArgumentException();
            else if (m == 0 || n == m)
                return 1;
            else
                return combin(n - 1, m) + combin(n - 1, m - 1);
        }
I had one more example for people that involved filling a region of pixels with a particular color. I had modified the DrawingPanel class from cse142 to allow me to draw freehand some curves inside the drawing panel. I then clicked on various regions that I wanted to have painted red. We found that the recursive code worked well in terms of filling in just the region. The only problem we found was that when the region was sufficiently large, we generated a stack overflow. Obviously recursion is not the best way to solve this problem, but it was interesting that a short recursive solution was producing the correct result. The code I was executing appears below.

        public void fill(BufferedImage image, int x, int y) {
            if (image.getRGB(x, y) == 0) {
                image.setRGB(x, y, Color.RED.getRGB());
                fill(image, x - 1, y);
                fill(image, x + 1, y);
                fill(image, x, y - 1);
                fill(image, x, y + 1);
            }
        }

Stuart Reges
Last modified: Wed Feb 1 15:57:48 PST 2006