CSE143X Notes for Friday, 10/18/24

We have been looking at code for a LinkedIntList that stores a list of ints. The constructor for the class constructs an empty list by setting the field called front to null:

        public LinkedIntList() {
            front = null;
        }
This is appropriate for a LinkedIntList in which you want to have the client add each value to the list one at a time. But the next homework assignment involves writing a constructor that initializes the list to have several values. So to practice that, we considered the task of writing a constructor that takes an integer parameter n and that constructs a list that has the numbers 0 through n in descending order (like a countdown). For example, if the client says:

        list = new LinkedIntList(10);
The list should be initialized to [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]. We considered several cases. If n is 0, we first have to construct a node. I decided to introduce a variable called temp to use for the node construction:

        ListNode temp;
        temp = new ListNode(0);
But this just constructs the node. After constructing it, we want to link it into our list. We do that by resetting front:

        front = temp;
What about when n is 1? We'd want to construct a node that has 1, as we did above, but this time we want its link to be to the front of the list. And we'd want to link that node into our list by resetting front again:

        temp = new ListNode(1, front);
        front = temp;
Similarly for the value 2, we'd say:

        temp = new ListNode(2, front);
        front = temp;
The general pattern here is that each time we add a node for the value i, we want to say:

        temp = new ListNode(i, front);
        front = temp;
The good thing about this code is that it makes it clear that this is a two-step process. First we construct a node using the old value of front and then we link it in by changing front. But the variable temp is not necessary. Experienced programmers would tend to write this as:

        front = new ListNode(i, front);
To understand this line of code, you have to remember that the right-hand side of an assignment statement is evaluated first. So Java will first construct the node using the current value of front, and only then will it reassign the variable front to point to the new node. This is the linked list equivalent of understanding how this assignment statement works:

        x = x + 1;
If we turn these into the one-line form, then we can see a pretty clear pattern. If you wanted to construct a list up to 5, you'd say:

        front = new ListNode(0);
        front = new ListNode(1, front);
        front = new ListNode(2, front);
        front = new ListNode(3, front);
        front = new ListNode(4, front);
        front = new ListNode(5, front);
The first line looks different from the others, but it actually fits the same pattern if you realize that front will be null for that first execution. So this pattern can be captured in a for loop:

        public LinkedIntList(int n) {
            front = null;
            for (int i = 0; i <= n; i++) {
                front = new ListNode(i, front);
            }
        }
It would be good to add a test to throw an exception when n is less than 0, but otherwise this completes the constructor.

Then we turned to another problem. Suppose we want to write a new method called addSorted that could be used to build up a list of values in sorted order. In other words, it would be specified as follows:

        // pre : list is in sorted (nondecreasing) order
        // post: given value is added to the list so as to preserve sorted
        //       (nondecreasing) order, duplicates allowed
        public void addSorted(int value) {
            ...
        }
Our goal is to fill in the code for the "..." and it was complicated enough that it took the rest of the lecture. Exploring this task points out important cases to consider and significant pitfalls that you have to be careful to avoid when programming linked lists.

We first looked at the "general" case of adding a value to the middle of a list. So suppose that the list currently stores the values (2, 5, 12):

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   2  |   +--+--->  |   5  |   +--+--->  |  12  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
and we call the addSorted method passing it the value 10. How do we add the value 10 to this list to preserve sorted order? First we need to find the right spot to insert it. People quickly said that it belongs between the 5 and 12. Why there? because it is larger than 5 and smaller than 12. And how do we write that as a loop? We have to compare the value against the various data values stored in the list starting with the first value. 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 reminded people of something I said in Wednesday's lecture that there are only two ways to change the contents of a list:

In this case, we want to change the "next" field of the node that has 5 in it. So 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;
        }
In effect, our test is now saying, "While the value that is one to the right of where I am now is less than value, keep advancing current." 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 need 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: 2, 5, 10, 12, which is what we want.

As with our constructor code, some people prefer to write the code for creating the new node in two steps with a temporary variable, as in:

        // first construct the node
        ListNode temp = new ListNode(value, current.next); 
        // then link it into the list
        current.next = temp;
There is no particular need to do this in two steps, but if it helps you to understand what is going on, you can write your code that way.

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 13? 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 42? 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. The test for current.next being null should 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 an 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.

So then we considered the case of inserting a value that belongs at the front. For example, suppose that we want to insert the value 1 in the previous list that begins with the value 2. The code we have written starts current at the front of the list and inserts the value after that node by changing the value of current.next. So the value 1 would be inserted after the value 2, which is clearly wrong.

For the "front of the list" case, we have to write code that changes front rather than changing current.next. When would we want to do that? Someone said when the value is less than front.data. And what do we want to do? We want to set front to a new list node that points at the old front of the list:

        if (value <= front.data) {
            front = new ListNode(value, front);
        }
The construction of the new node could again be written as a 2-step process, although the code above works just fine:

        if (value <= front.data) {
            ListNode temp = new ListNode(value, front);
            front = temp;
        }
There was yet another case. If we ran this program we'd find that after all of our hard work, the program throws a NullPointerException on the very first call to addSorted. That would happen because we never thought about the case of an empty list. If the list is empty, then the code above throws a NullPointerException when we ask for front.data because front will be null.

So we need yet another test:

        if (value <= front.data || front == null) {
            front = new ListNode(value, front);
        }
Of course, I purposely wrote this in the wrong way as well. This is another example of a sensitive test (referring to front.data) and a robust test (testing front for null). So we have to reverse the order of these two tests to make it work properly.

Putting this all together, we end up with the following solution that is included in handout 5:

        if (front == null || value <= front.data) {
            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 if statement in this code 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 || value <= front.data)
            ~~~~~~~~~~~~~    ~~~~~~~~~~~~~~~~~~~
             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 #5 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: Fri Oct 18 15:32:43 PDT 2024