CSE143 Notes for Wednesday, 2/24/16

I started a new topic: binary search trees. They are a particular kind of binary tree that have a special property that is sometimes referred to as the binary search tree property. In particular, we want to guarantee that for every subtree, the following relationship holds:

                     +-----------+
                     | root data |
                     +-----------+
                         /   \
                       /       \
                     /           \
        +----------------+   +---------------+
        | values <= data |   | values > data |
        +----------------+   +---------------+
In other words, the tree is structured so that values that appear in the left subtree are all less than or equal to the root data and values that appear in the right subtree are all greater than the root data. And remember that this property is preserved throughout the tree, not just for the overall root.

I talked a little about the issue of duplicate values. You can adopt one of several different conventions. For example, you might decide that duplicates aren't allowed. In my the examples I am using, I allow duplicates and I decided that they should be inserted into the left subtree. I could just as well have decided that duplicates go into the right subtree. The key thing is to pick a convention and be consistent.

I asked people to give me several names and we used them to construct a binary search tree. As we inserted values into the tree, we kept finding ourselves positioned at a particular node of the tree and we'd ask ourselves, "Is the value that we are inserting alphabetically less than the value in this node or alphabetically greater?" If it was alphabetically less, then it was inserted to the left. If it was alphabetically greater, it was inserted to the right.

When we performed an inorder traversal on the final tree, we got an interesting result. The list of names appeared in alphabetical order. This is one of the aspects of a binary search tree that makes it so interesting.

This isn't terribly surprising when you think about how the tree was constructed. We constructed the tree so that values that are alphabetically less than or equal to the root appear in the left subtree and values alphabetically greater appear in the right subtree:

An inorder traversal traverses the left subtree first, then the root, then the right subtree. So if we were to print these values, we would get them in this order:

        (values <= data)   root data   (values > data)
        \------1st-----/   \--2nd--/   \-----3rd-----/
That means that the root data is printed at just the right point in time (after the values that are alphabetically less than it and before the values that are alphabetically greater than it). But we repeated this pattern throughout the tree when we constructed it, so every node has this property. That means that the data stored in each node is printed in the correct position relative to the values that come below it. The overall result is that each data value is printed at just the right point in time relative to the rest of the data.

Then I talked about how to write code to implement this. For our purposes, we considered the problem of adding values to the IntTree class we discussed in Monday's lecture. We want to write client code like the following:

        Scanner console = new Scanner(System.in);
        IntTree numbers = new IntTree();
        System.out.print("Next int (0 to quit)? ");
        int number = console.nextInt();
        while (number != 0) {
            numbers.add(number);
            System.out.print("Next int (0 to quit)? ");
            number = console.nextInt();
        }
To make this work, we have to add a zero-argument constructor that constructs and empty tree:

        public IntTree() {
            overallRoot = null;
        }
Then we have to write the add method. It needs to do what we saw the program doing: inserting each new value into the tree so as to preserve the binary search tree property.

The add method will have the usual structure of a public method that the client calls with no mention of tree nodes and a private recursive method that takes a node as a parameter and that does the actual work. So our pair of methods will look like this:

        public void add(int value) {
            add(overallRoot, value);
        }

        private void add(IntTreeNode root, int value) {
            ...
        }
I again said to think in terms of the definition of a tree. A binary tree is either empty or it is a root node with left and right subtrees. If it is empty, then we want to insert the value here. For example, initially the overall tree is empty and we insert the first value at the top of the tree (replacing the "null" value with a reference to a new leaf node with the given value in it). So the private add method would look like this:

        private void add(IntTreeNode root, int value) {
            if (root == null) {
                root = new IntTreeNode(value);
            } ...
        }
But what if it's not an empty tree? Remember that when we built up the tree ourselves, we compared the value at the root with the value we are inserting and we either went left or went right depending upon how that comparison went. So we want something like this:

        private void add(IntTreeNode root, int value) {
            if (root == null) {
                root = new IntTreeNode(value);
            } else if (value <= root.data) {
                // add to left
            } else {
                // add to right
            }
        }
This is the general structure that we want. So how do we add to the left or add to the right? Some novices find themselves wanting to test things about the left or right subtree, as in:

        private void add(IntTreeNode root, int value) {
            if (root == null) {
                root = new IntTreeNode(value);
            } else if (value <= root.data) {
                if (root.left == null) {
                    root.left = new IntTreeNode(value);
                } else { // even more stuff
            } else {
                // add to right
            }
        }
This is not a good way to approach the problem. If you're thinking recursively, you'll realize that it's another insertion task into either the left subtree or the right subtree. So you can call the add method itself:

        private void add(IntTreeNode root, int value) {
            if (root == null) {
                root = new IntTreeNode(value);
            } else if (value <= root.data) {
                add(root.left, value);
            } else {
                add(root.right, value);
            }
        }
The logic of this code is almost correct. Unfortunately, in this form the tree is always empty. The add method never inserts a single value. The problem has to do with the parameter called "root". The parameter "root" will store a copy of whatever is passed into it. As a result, when we reassign root, it has no effect on the value passed into it.

To explore this further, I asked people to consider the following short program:

        import java.awt.*;
        
        public class PointTest {
            public static void main(String[] args) {
                Point x = new Point(2, 8);
                System.out.println("x = " + x);
                change(x);
                System.out.println("now x = " + x);
            }
        
            public static void change(Point p) {
                p.translate(3, 5);
                System.out.println("p = " + p);
                p = new Point(-7, -14);
                System.out.println("now p = " + p);
            }
        }
The code in main constructs a Point object and passes it as a parameter. Inside the method we translate the coordinates of the Point object. The question is whether this change is reflected in the original Point object. The answer is yes. This happens because when we pass an object as a parameter, the parameter gets a copy of the reference to the object.

But what about the final line of the method that constructs a new point? Does that change the main method's variable x? The answer is no. The overall output for this version of the program is:

        x = java.awt.Point[x=2,y=8]
        p = java.awt.Point[x=5,y=13]
        now p = java.awt.Point[x=-7,y=-14]
        now x = java.awt.Point[x=5,y=13]
One of the changes has an effect (translating the coordinates), but not the other (constructing a new Point). This happens because when we pass an object as a parameter, the parameter gets a copy of the reference to the object. So when we first call the change method, the variable p is set up as a copy of the variable x:

                   +--------------------+
          +---+    |   +----+    +----+ |
        x | +-+--> | x |  2 |  y |  8 | |
          +---+    |   +----+    +----+ |
                   +--------------------+
                              ^
           +---+              |
         p | --+--------------+ 
           +---+
Using the cell phone analogy I've been using, both x and p are calling the same cell phone (talking to the same object). That's why the method is able to change the object that x refers to when it executes these this line of code:
        p.translate(3, 5);
We have two different variables both referring to the same object, so either variable is capable of changing the object:

                   +--------------------+
          +---+    |   +----+    +----+ |
        x | +-+--> | x |  5 |  y | 13 | |
          +---+    |   +----+    +----+ |
                   +--------------------+
                              ^
           +---+              |
         p | --+--------------+ 
           +---+
But that doesn't mean that p has the power to change x itself. It can change the object that x refers to (the Point), but it can't change the value that is stored in x (the reference or pointer or arrow or cell phone number). So when we change p with the following line of code, it has no effect on x:

        p = new Point(-7, -14);
This gives a new value to p, but not to x:

                   +--------------------+
          +---+    |   +----+    +----+ |
        x | +-+--> | x |  5 |  y | 13 | |
          +---+    |   +----+    +----+ |
                   +--------------------+

                    +--------------------+
           +---+    |   +----+    +----+ |
         p | +-+--> | x | -7 |  y |-14 | |
           +---+    |   +----+    +----+ |
                    +--------------------+
There are many ways to try to fix this, but there is a particular approach that works well for binary tree code. To understand it, I first spent some time returning to our cell phone analogy for references. Suppose that I ask you to get out your cell phone and look up the number of someone you call often. You tell me the number and I write it down like this:

        (206) 555-1234
Because I have the number written down, I can call that person and talk to them. If I call your friend, then the next time you talk to the friend, they are likely to mention that I called. In other words, I can change your friend's "state." This is like knowing the reference to an object and using it to call methods like the Point object's translate method.

But suppose that I cross off that number and write down a new number like this:

        (206) 555-5555
Writing this on my piece of paper has no effect on your cell phone. I had a copy of your friend's phone number, so changing my copy doesn't change your original version of the number stored in your cell phone. This is like what we saw with the main program's variable x that was unchanged even though the parameter p was changed. The parameter p is a copy, so changing it doesn't change the original.

Now think about the two of us as being Java methods. You are one method calling Stuart (another method). You give information to Stuart using a parameter. How does Stuart get information back to you? We discussed this in 142. We use a return value. Let's make that change in the change method:

        public Point change(Point p) {
            p.translate(3, 5);
            p = new Point(-7, -14);
            System.out.println("p = " + p);
            return p;
        }
Two things changed here. We added a return at the end and we changed the return type of the method. So does this cause it to now update the variable x in main? No. Remember that it isn't enough for a method to return a value. We have to change the call on the method so that it does something with the value being passed back. This is the line that calls the method:

        change(x);
The method is passing back a new value, but we have to assign it somewhere. The idea is to update what x is referring to, so we assign it back into x:

        x = change(x);
We refer to this technique as the "x=change(x)" approach. With these changes, the program produces the following output:

        x = java.awt.Point[x=2,y=8]
        p = java.awt.Point[x=5,y=13]
        now p = java.awt.Point[x=-7,y=-14]
        now x = java.awt.Point[x=-7,y=-14]
Now the change that occurs in the method propagates back to the main method.

Then I explored yet another cell phone scenario. Suppose I ask you what you have stored in your cell phone for Stuart's cell phone number. You have nothing. So you tell me it is "null" and I write that down:

        null
It should be null because Stuart doesn't own a cell phone. But suppose Stuart goes out and gets one and writes down the number:

        (206) 555-3333
Now Stuart knows his new cell phone number, but you don't. Stuart would have to tell you the new number and you would have to update your entry for him, replacing the null with the phone number. If we again think of this as you being a method and calling Stuart, then Stuart would have to return the value back to you and you would have to reassign your entry for Stuart so that it would replace the old value of "null" with this new phone number. We're going to use the x=change(x) approach to make that happen and it's exactly what we need for our binary search tree.

Currently the add method has a return type of void:

        private void add(IntTreeNode root, int value) {
            ...
        }
We can change it so that the last thing we do in the method is to return the value of root, which means we have to change the return type to IntTreeNode:

        private IntTreeNode add(IntTreeNode root, int value) {
            ...
            return root;
        }
Then we change every call on add to match the "x = change(x)" form. For example, our old public method:

        public void add(int value) {
           add(overallRoot, value);
        }
becomes:
        public void add(int value) {
           overallRoot = add(overallRoot, value);
        }
The idea is that we pass in the value of overallRoot to the add method and it passes back the value of the parameter, which might be the old value or it might be a new value. We reassign overallRoot to this value passed back by add. That way, if the method changes the value of the parameter, then overallRoot gets updated to that new value. If it doesn't change the value of overallRoot, then we are simply assigning a variable to the value it already has (effectively saying "x = x"), which has no effect.

There are two other calls on add inside the method itself that need to be updated in a similar manner:

        private IntTreeNode add(IntTreeNode root, int value) {
            if (root == null) {
                root = new IntTreeNode(value);
            } else if (value <= root.data) {
                root.left = add(root.left, value);
            } else {
                root.right = add(root.right, value);
            }
            return root;
        }
With these changes, the method worked properly which we were able to verify with some client code that added various values to a tree and then performed an inorder traversal to print them out in sorted order.

I mentioned that this "x = change(x)" idiom is very powerful and simplifies a lot of binary tree code, so it is important to learn how to use it well.


Stuart Reges
Last modified: Wed Feb 24 14:34:41 PST 2016