CSE143 Notes for Wednesday, 3/30/08

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,926. 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,926. Someone said it ends in 0. How do you know? Because it's an even number. Even numbers end in 0 in base-2 and odd numbers end in 1 (just look at the list above and see how the final digit alternates).

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 #13:

        public 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 discussed how we might 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 pointed out 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) {
            ...
        }
The base case (the simplest power to compute) is when y is 0, in which 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)
                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)
                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.

I then asked people what this method would do if we passed a negative value of y. People said that it would go into infinite recursion. That's obviously not desirable behavior, but we do have a precondition saying that you should call this method with a negative exponent. 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.

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 232, whether they would really use their calculator to say 2 * 2 * 2 * 2 * 2 * 2 * 2, and so on, 32 different times. Isn't there a better way? Someone mentioned that we could compute (216)2. I said that we also could compute (22)16. That would lead to this formula:

xy = (x2)y/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 y. 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. How often would we 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.

Then I went to the computer to consider a problem that will be more like the homework assignment that will be assigned after the midterm. The idea is to prompt the user for the name of a file or directory and to print out that name along with any files that are underneath it. In particular, if it's a directory, we want to include everything inside the directory.

Java has a class called File that is part of the java.io package that provides the functionality we need to solve this problem. I put up this starter code for us:

        import java.io.*;
        import java.util.*;
        
        public class Crawler {
            public static void main(String[] args) {
                Scanner console = new Scanner(System.in);
                System.out.print("directory or file name? ");
                String name = console.nextLine();
                File f = new File(name);
                if (!f.exists()) {
                    System.out.println("Does not exist");
                } else {
                    print(f);
                }
            }
        
            public static void print(File f) {
                System.out.println(f.getName());
            }
        }
This is the boring prompting part of the program. The real work is in writing the print method that is supposed to print everything. In the starter code, we simply print the name of the given file. This worked fairly well. For example, when I ran it and typed in "/Users/reges/143/crawler/Crawler.java", it printed:

        Crawler.java
This is the name of the file we were writing and it's what we want it to do for a file name. Then I typed in the directory name "/Users/reges/143/crawler" it printed:

        crawler
This is good in that it is printing the name of the crawler directory, but it wasn't showing any of the files and subdirectories inside the directory. One thing to keep in in mind is that the File class in Java is used to store information about both files and directories. In the first case, we were given a File object that was linked to a file. But in this second case, we have a File object linked to a directory.

I mentioned that there is a method called listFiles that will return a list of the files in a directory. The return type is File[]. So we might be tempted to write code like this:

        public static void print(File f) {
            System.out.println(f.getName());
            File[] files = f.listFiles();
            for (int i = 0; i < f.length; i++) {
                // do something with files[i]
            }
        }
That approach works, but I pointed out that this is a good place to use a for-each loop. We don't particularly care that the files are stored in an array. We only care that they are in some kind of list structure that we can iterate over. The for-each loop allows us to do this with simpler syntax:

        public static void print(File f) {
            System.out.println(f.getName());
            for (File subF : f.listFiles()) {
                // do something with subF
            }
        }
When we describe a for-each loop like this, we read it as, "for each File subF in f.listFiles()...". You need to come up with a variable name in the for-each loop. I couldn't use "f" because I was already using that for the name of the File passed to the method as a parameter. I could have used any name I want ("x", "foo", etc). I used the name "subF" because it seemed to me like a good way to indicate that this is a subfile of the original File object.

What we want to do with this is to print each subfile name with some indentation. So we can complete this with a simple println:

        public static void print(File f) {
            System.out.println(f.getName());
            for (File subF : f.listFiles()) {
                System.out.println("    " + subF.getName());
            }
        }
This version worked nicely. When I typed in "/Users/reges/143/crawler", it showed the name of the directory along with a list of the files and subdirectories inside the directory

crawler
    Crawler.class
    Crawler.java
    Crawler.java~
    dirA
    dirB
    dirC
    DirectoryCrawler.class
    DirectoryCrawler.java
    Fun.java
In fact, this is the same list we saw when I clicked on "open" in DrJava and asked it to show me a list of the contents of the directory. So far so good.

But when we ran it again and asked it to print information for "/Users/reges/143/crawler/Crawler.java", we got a NullPointerException. Remember that sometimes the method is going to be asked to print a File object tied to a directory and sometimes it's going to be asked to print a File object that is tied to a simple file. For the simple file case, when we call f.listFiles(), we get the value null returned. So then when we try to use it in a for-each loop, we get the NullPointerException thrown.

We fixed this by including a test on whether or not the File object is a directory. We found just what we needed in the File class: a method called isDirectory that returns a boolean result. So we rewrote print to be:

        public static void print(File f) {
            System.out.println(f.getName());
            if (f.isDirectory()) {
                for (File subF : f.listFiles()) {
                    System.out.println("    " + subF.getName());
                }
            }
        }
At this point we had a pretty good solution using iterative techniques. But we're still far from a working solution. I ran the program again and typed in "/Users/reges/143/crawler". It printed the "crawler" name and printed the contents of the directory, but it didn't show everything. For example, I have a subdirectory called "dirA" and it wasn't showing the contents of that. Why not? Because there are files and subdirectories inside that directory (or folders inside that folder if you prefer the desktop terminology). Our code doesn't handle directories inside of directories, so we have to fix it.

This is a very crucial point in the development of this program. In fact, if there is anything that I would recommend going over several times, it is what is about to happen right here. This is the crucial insight as to how to apply recursion here and it will be the most useful guide to help you understand what you'll need to do for the next programming assignment.

So let's review where we are at. We have a method called print that is passed a File parameter called f. If f is a directory, we use a for-each loop to iterate over its contents, storing each one in a File variable called subF. The problem is that some of the subF objects are themselves directories that need to be expanded. Your instinct, then, might be to do something like this:

        public static void print(File f) {
            System.out.println(f.getName());
            if (f.isDirectory()) {
                for (File subF : f.listFiles()) {
                    System.out.println(subF.getName());
                    if (subF.isDirectory()) {
                        ...
                    }
                }
            }
        }
This is not the right way to think about the problem. And it won't work. What if we had an inner for-each loop with a subsubF variable? Would that solve the problem? No, because one of those subsubF objects might be a directory. So we'd need another test and another loop with a subsubsubF variable. Would that work? No, because one of those might be a directory. We can't solve this problem easily with simple iterative techniques. The easier way to solve it is to think about this problem recursively.

To think about it recursively, think about the method we are writing. It is supposed to print information about either a file or a directory. If it is printing information about a directory, then it prints the contents of the directory. Our problem is that those contents might be either files or directories, which have to be handled differently.

Often with a recursive problem, the answer will be staring you right in the face if you can just connect the dots. We find ourselves inside the for-each loop needing to print information about either a file or a directory. As I keep saying in lecture (to make fun of how easy this is when you can just look at it the right way), "If only we had a method that would print information about a file or directory, we could call it...But we do have such a method...it's the print method we're writing."

We can almost make this code work by replacing the println with a call on print:

        public static void print(File f) {
            System.out.println(f.getName());
            if (f.isDirectory()) {
                for (File subF : f.listFiles()) {
                    print(subF);
                }
            }
        }
We ran this version and when we asked for the contents of "/Users/reges/143/crawler" we saw a long list as it printed every single file and directory that is found inside that directory.

I've been using an almost joking tone when I say, "If only we had a method that would..." I don't mean to imply that this is somehow obvious. It takes time to master recursion, so you should expect that you'll struggle as you work to understand this. The point I am trying to make is that the answer is probably simpler than you think it is. There is an elegance to recursion that allows you to solve very complex problems with just a few lines of code. So when you struggle to understand this, be sure to consider the possibility that the answer is actually much simpler than you realize if you can just think about it the right way.

I mentioned earlier that this solution is almost right. There are two problems. First, it should indent things to indicate what is inside of what. The problem is that we need different levels of indentation. We solved this by adding a new parameter called level to the method and using it in a for loop to print indentation:

        public static void print(File f, int level) {
            for (int i = 0; i < level; i++) {
                System.out.print("    ");
            }
            System.out.println(f.getName());
            ...
        }
This required us to change the call in main to include a level:

        print(f, 0);
We also had to change the recursive call inside of print to include a value for the level parameter:

        print(subF, level + 1);
It is important to use an expression like (level + 1) versus saying level++. In writing iterative solutions, we often use expressions like level++, but they tend not to work well in recursive solutions. In this case, it would cause successive elements of a directory to be printed with increasing indentation, moving diagonally to the right rather than lining up.

When we ran the new version with "/Users/reges/143/crawler" we saw this list of files and subdirectories, with indentation indicating what is inside of what:

        crawler
            Crawler.class
            Crawler.java
            Crawler.java~
            dirA
                data.txt
                dirA1
                    fun.txt
                    fun2.txt
                dirA2
                    dirA2a
                        deep.txt
                        deep2.txt
                    help.txt
            dirB
                dirB1
                    Fun.class
                    Fun.java
                dirB2
                dirB3
                    dirB3a
                        answers.txt
                        howdy.txt
                    hw4.txt
                paper.doc
                sample.txt
            dirC
                special.txt
            DirectoryCrawler.class
            DirectoryCrawler.java
Because the code is written recursively, the crawler can handle any level of nesting that we encounter.

Then I showed people what is known as the Sierpinski fractal. The idea is to start with a simple triangle. You then replace the triangle with three smaller triangles that appear inside it. Then you replace each of those triangles with three similar triangles. And you keep doing this kind of replacement. The theoretical fractal has no limit to how many replacements are performed, but in practice we can imagine different levels of replacement. The program I showed prompts the user for the level to use for replacement. Below is the result for level 7:

The program itself is included as part of handout #14 and a detailed discussion of the program is included in chapter 12 of the textbook.


Stuart Reges
Last modified: Thu May 1 10:44:48 PDT 2008