CSE143 Notes for Friday, 1/20/06

I began by discussing a certain code fragment that often confuses novices. Suppose that we have a list of three elements:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   2  |   +--+--->  |   5  |   +--+--->  |  12  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
And we wanted to append the value 4 at the end of the list using the following code:

        ListNode current = front;
        while (current != null)
            current = current.next;
        current = new ListNode(4);
This code doesn't change the list at all. Instead, it leaves us in this situation:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   2  |   +--+--->  |   5  |   +--+--->  |  12  |   /  |
         +---+      +------+------+      +------+------+      +------+------+

                    +------+------+
         +---+      | data | next |
 current | +-+--->  |   4  |   /  |
         +---+      +------+------+
It's true that current scans through the list until it reaches the end and then we allocate a new node, but this new node is not connected to the list. I gave 2 analogies for understanding this.

Think about trying to sneak friends into a movie theater. A friend enters the theater the normal way, walks through the theater, then goes out an exit door. But they let the door shut behind them. They say, "Now we can sneak in," and you say, "No we can't because you let the door shut behind you." The friend was supposed to open the door, but stay in the doorway. Even though he was just in the right spot to allow you to get into the theater, he went too far and lost his chance to let you in.

As a second analogy, I mentioned the idea that you can think of the list nodes as being like railroad cars. You can drag the entire train by dragging the front car around, sort of the way we keep track of the front of the list to keep track of the whole thing. I then said to imagine the variable current as being a little like Sean Connery as James Bond. He starts out standing on top of the front car and then he leaps to the car behind that one (that's what happens when you set current to current.next). Then he jumps off the second car onto the third car. Then he jumps off the third car onto the tracks. At that point he notices that he has found the end of the train. But by jumping off the last car, he's jumped off the train. The train would be speeding off into the distance and he'd be standing there saying, "Come back. I want to attach a new car at the end."

I reminded people that there are only two ways that you can change the contents of a list:

To solve this problem, we have to stop one position early. We don't want to run off the end of the list as we did with the printing code. Instead, we want to position current to the final element. We can do this by changing our test. Instead of going until current becomes null, we want to go until current.next is null, because only the last node of the list will have a next field that is null:

        ListNode current = front;
        while (current.next != null)
            current = current.next;
        current.next = new ListNode(4);
The code above will correctly append a value to the end of the list.

Then I turned to a new example. How can we "addSorted"? Suppose the list is in sorted order and we want to add a new value so as to preserve the sorted order. So suppose we have a variable called "value" that is 10 and we want to insert it into this list:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   2  |   +--+--->  |   5  |   +--+--->  |  12  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
How do we do it? We need to find the right spot to insert it and that is going to depend on the value we are inserting (10 in our example). We have to compare it against the various data values stored in this list. The new node doesn't belong in front of the node with 2 in it because 2 is less than 10. Similarly it doesn't belong in front of the node with 5 in it because 5 is less than 10. But it does belong in front of the node with 12 in it because 12 is not less than 10. We can make a stab at the code as follows:

        ListNode current = front;
        while (current.data < value)
            current = current.next;
This has the core of the right idea, but it has many problems. First of all, it ends up positioning us in the wrong spot. As in the appending case, we want to stop one position early to be able to add something to the list. I mentioned that there is a variation of linked lists called a "doubly linked list" where you can move backwards and forwards in the list, but we can't do that with these simple lists that are linked in just one direction.

The point is that we don't want to have our variable current end up referring to the node that has 12 in it. We have to have current pointing to the node that has 5 in it. So we have to modify the code to stop one position early. This can be done by changing the test to involve "current.next" instead of "current":

        ListNode current = front;
        while (current.next.data < value)
            current = current.next;
This theoretically stops with current referring to the node with 5 in it, which means we can link in the new node by changing current.next. This new node should have a data value of "value" (10 in our example). What should its next link refer to? The answer is that it should refer to the node that has 12 in it, which is stored in current.next. So we'll want to construct the node in this way:
        new ListNode(value, current.next)
Just calling the constructor leaves us in this situation:

                                                      +------+------+
                                                      | data | next |
                                                      |  10  |   +  |
                                                      +------+---|--+
                                                                 |
                                                                 V
                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   2  |   +--+--->  |   5  |   +--+--->  |  12  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                                                ^
         +---+                                  |
 current | +-+------>------>------>------>------+
         +---+
This isn't enough. We've constructed a node that points at the list, but nothing in the list points at the node. So we've taken care of half of what we have to do. The other half is to change a link of the list to point to the new node. The link to change is current.next:

        current.next = new ListNode(value, current.next);
which leads to this situation:

                                                      +------+------+
                                                      | data | next |
                                                      |  10  |   +  |
                                                      +------+---|--+
                                                           ^     |
                                                           |     V
                    +------+------+      +------+------+   |  +------+------+
         +---+      | data | next |      | data | next |   |  | data | next |
   front | +-+--->  |   2  |   +--+--->  |   5  |   +--+---+  |  12  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                                                ^
         +---+                                  |
 current | +-+------>------>------>------>------+
         +---+
This isn't the easiest picture to read, but if you follow the links carefully, you'll see that starting at front the sequence of values you see is: 2, 5, 10, 12, which is what we want.

So the code sort of works. But there are some special cases we haven't thought about. What if you want to insert the value 15? Remember our loop test:

        while (current.next.data < value)
This depends on finding a value in the list that is less than the value we are trying to insert. What if there is no such value, as in the case of inserting 15? This code keeps moving current forward until current.next is null. At that point, when we try to ask for the value of current.next.data, we are asking for null.data, which throws a NullPointerException because there is no object to find the data field of.

If the value is greater than everything else in the list, then it belongs after the last node in the list. So we want to stop when current gets to the last node in the list. So a second attempt at the test would be:

        while (current.next.data < value && current.next != null)
But even this doesn't work. It would stop current at the right place, but when current.next is null, we can't ask for the value of current.next.data. That test will throw a NullPointerException. This is another example of a combination of a sensitive and robust test:

        while (current.next.data < value && current.next != null)
               ~~~~~~~~~~~~~~~~~~~~~~~~~    ~~~~~~~~~~~~~~~~~~~~
                    sensitive test              robust test
We need to switch the order of these tests to make them work right.

        while (current.next != null && current.next.data < value)
Java uses what is known as "short-circuited evaluation," which means that if the first test evaluates to false, Java doesn't bother to perform the second test. So the first test, in effect, protects you from the potential problem generated by the second test (the null pointer exception).

Putting this all together, we have the following solution to the problem:

        ListNode current = front;
        while (current.next != null && current.next.data < value)
            current = current.next;
        current.next = new ListNode(value, current.next);
But even this code is not enough. I referred to the first test in this loop as the robust test, but it isn't all that robust. If current is null, then it throws a null pointer exception. So we want to execute this code only in the case where front isn't null.

There was another case as well. Someone pointed out that if the value belongs at the front of the list, then this code places it in the wrong spot. It always inserts after a node currently in the list, never placing it in front of all nodes.

We didn't have time to work the code out in detail, so I pointed people to the solution from handout #8:

        if (front == null || front.data >= value)
            front = new ListNode(value, front);
        else {
            ListNode current = front;
            while (current.next != null && current.next.data < value)
                current = current.next;
            current.next = new ListNode(value, current.next);
        }
The initial test deals with the two special cases just mentioned. If the list is currently empty (front == null) or if the value belongs at the front of the list (front.data >= value), then we insert at the front of the list rather than using the other code we developed. Order is important in this test as well because the test involving front.data will throw a NullPointerException if front is null.

I pointed out that this is a good example to study because it has so many special cases. In writing our code we had to deal with:

The first two of these cases are handled by the "if" branch of the code:

        if (front == null || front.data >= value)
            ~~~~~~~~~~~~~    ~~~~~~~~~~~~~~~~~~~
             empty list         front of list
and the second two cases are handled in the "else" branch of the code:

        while (current.next != null && current.next.data < value)
               ~~~~~~~~~~~~~~~~~~~~    ~~~~~~~~~~~~~~~~~~~~~~~~~
                   back of list             middle of list
I briefly mentioned that handout #8 has a second solution to this problem that involves keeping a 2-element window on the list using variables called prev and current.

                         +-+-+                +-+-+
                    prev | + |        current | + |
                         +---+                +---+
                           |                    |          
                           V                    V
                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   2  |   +--+--->  |  5   |   +--+--->  |  12  |   +  |
         +---+      +------+------+      +------+------+      +------+---+--+
As an analogy, consider the idea of an inchworm that is two nodes in length. When stretched out, the back half of the inchworm would be on one node and the front half would be on the next node over. When the inchworm goes to move forward, it scoots its back half up to where the front half is, then scoots the front half onto the next node. This is exactly the code you'd use to move this pair of variables forward one spot:

        prev = current;
        current = current.next;
Some people prefer this approach to looking one ahead with expressions like "current.next.data".


Stuart Reges
Last modified: Sun Jan 22 16:54:24 PST 2006