CSE143X Notes for Wednesday, 10/18/23

I continued our discussion of linked lists by considering how to write code that would print the values in a list one per line. For example, suppose we have a variable called list that stores a reference to the list [3, 5, 2]:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
Using the techniques from Monday's lecture we could refer to each of the three data fields: list.data (3), list.next.data (5) and list.next.next.data (2). This can work for very short lists, but obviously won't work when we have hundreds or thousands of nodes to process. So we want to write a loop for this.

We have just one variable to work with, so that's clearly where we have to start (the variable "list"). We could use it to move along the list and print things out, but then we would lose the original value of the variable which would mean that we would have lost the list. Instead, we declare a local variable of type ListNode that we will use to access the different data fields of the list:

        ListNode current = list;
This initializes current to point to the same value as list (the first node in the list). We want to have a loop that prints the various values and we want it to keep going as long as there is more data to print. After executing the statement above, we have the following situation:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                           ^
         +---+             |
 current | +-+------>------+
         +---+
So how do we structure our loop? We want to keep going while there is more data to print. The variable current will end up referring to each different node in turn. The final node has the value null in its next field, so eventually the variable current will become null and that's when we know we're done. So our basic loop structure will be:

ListNode current = list; while (current != null) { <process next value> } To process a node, we need to print out its value, which we can get from current.data, and we need to move current to the next node over. The position of the next node is stored in current.next, so moving to that next node involves resetting current to current.next:

        ListNode current = list;
        while (current != null) {
            System.out.println(current.data);
            current = current.next;
        }
The first time through this loop, current is referring to the node with the 3 in it. It prints this value and then resets current, which causes current to refer to (or point to) the second node in the list:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                                                ^
         +---+                                  |
 current | +-+------>------>------>------>------+
         +---+
Some people prefer to visualize this differently. Instead of thinking of the variable current as sitting still while its arrow moves, some people prefer to think of the variable itself moving. So for the initial situation they'd draw this picture:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                           ^
                           |          
                         +-+-+
                 current | + |
                         +---+
And after executing the statement "current = current.next", we'd have this situation:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                                                ^
                                                |          
                                              +-+-+
                                      current | + |
                                              +---+
Either way of thinking about this works. Because in this new situation the variable current is not null, we once again go into the loop and print out current.data (which is now 5), and move current along again:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                                                                     ^
         +---+                                                       |
 current | +-+------>------>------>------>------>------>------>------+
         +---+
Once again current is not null, so we go into the loop a third time and print the value of current.data (2) and reset current. But this time current.next has the value null, so when we reset current we get:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+

         +---+
 current | / |
         +---+
Because current has become null, we break out of the loop having produced the following output:

        3
        5
        2
I pointed out that the corresponding array code would look like this:

        for (int i = 0; i < list.length; i++) {
            System.out.println(list[i]);
        }
Assuming you have some comfort with array-style programming, this might give you some useful insight into linked list programming. There are direct parallels here in terms of typical code:

Array/List Equivalents
Description Array code Linked list code
go to front of the list int i = 0; ListNode current = list;
test for more elements i < list.length current != null
current value list[i] current.data
go to next element i++; current = current.next;

In fact, knowing that we like to use for loops for array processing, you can imagine writing for loops for the processing of linked lists as well. Our code above could be rewritten as:

        for(ListNode current = list; current != null; current = current.next) {
            System.out.println(current.data);
        }
Some people like to write their list code this way. I tend to use while loops for list code, but it's an issue of personal taste.

Then I spent some time talking about how we are going to use linked lists to define a new class called LinkedIntList that will have the same methods as the ArrayIntList class. Instead of storing values with an an array and a size, we will store them in a linked list.

I asked what data fields will be needed and there were several suggestions. I said that for now we'll stick with the minimum and in this case the only data field you need is a reference to the front of the list:

public class LinkedIntList { private ListNode front; <methods> } Then I reminded people that our node class has public fields, which in general is a bad idea. It's not of great concern to us because we're going to make sure that only our LinkedIntList object will ever manipulate individual nodes. By the time we are done, we are going to have two classes: one for individual nodes of a list and one for the list itself. We'll be careful to have a clean, well-encapsulated list object, but we don't have to worry about doing the same thing for the node class.

I made the following analogy. Suppose I want to hire someone to paint my apartment. One contractor tells me I'll be carrying messy cans of paint around and another says I won't have to worry about that. Given that choice, I'd rather have the contractor who said I wouldn't have to worry about that. It's important to me that I stay clean. That's like a client talking to our list class. We'll want to make sure that the client has a clean interface. But I don't particularly care if the people doing the actual painting of my apartment get dirty. If one contractor told me he had bought some fancy paint cans that would keep his workers clean and that he was going to charge me more for them, I'd say I don't want to pay the extra money. I'd rather go with a cheaper contractor who uses messy cans of paint as long as I personally don't get dirty. So it's okay for the list itself to deal with these "messy" list node objects as long as the client of the list never sees them.

The "right" way to do this would be to declare the node class inside the list class itself. We'd make it a static inner class. But we haven't talked about the idea of an inner class, let alone the idea of a static inner class, so we'll keep the node in a separate class for now.

Next I discussed how to implement a method of the LinkedIntList class calld stutter. The idea is that it replaces every value in the list with two of that value. So if the list currently stores these values:

        [3, 5, 2]
and you call stutter, then it store these values afterwards:

        [3, 3, 5, 5, 2, 2]
I started with code following the pattern we saw before:

        public void stutter() {
            ListNode current = front;
            while (current != null) {
                ...
            }
        }
Think of what happens when the list stores [3, 5, 2]:

                        current
                           |
                           V
                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
We want to make a copy of the node with 3 in it and insert it into the list. It's generally easier to insert to the right rather than to the left, so the idea would be to construct a node that will point at the node with 5 in it and storing 3:

                                 +------+------+
                        current  | data | next |
                           |     |   3  |   +  |
                           |     +------+---+--+
                           |                |
                           V                V
                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
To construct a node like this, we would make this call on new:

        new ListNode(current.data, current.next);
That's half of the job...constructing the new node. But how do we link it in to the structure? The answer is that we can reassign current.next:

        current.next = new ListNode(current.data, current.next);
That would leave us in this situation:

                                 +------+------+
                        current  | data | next |
                           |  +->|   3  |   +  |
                           |  |  +------+---+--+
                           |  |            |
                           V  |            V
                    +------+--+---+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |   3  |      |      |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
The usual update would be to set current to current.next, but that would leave current positioned at the new node we just constructed. We want to position ourselves beyond that node, so we need to reset current to current.next.next.

        public void stutter() {
            ListNode current = front;
            while (current != null) {
                current.next = new ListNode(current.data, current.next);
                current = current.next.next;
            }
        }
Next I turned to the question of how we would implement the appending add operation from the old ArrayIntList class for our new LinkedIntList class that will look like this:

        public void add(int value) {
            ...
        }
The method is supposed to append the new value at the end of the list, which means we have to locate the end of the list. Let's think about the general case where we are appending to the end of a list that already has something in it. For example, suppose the list stores [3, 5, 2]:

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

         +---+
 current | / |
         +---+
and we want to add the value 17 at the end of the list. First we have to get there. So here's a start:

        ListNode current = front;
        while (current != null) {
            current = current.next;
        }
What happens is that the variable current moves along the list from the first to the last node until it becomes null, leaving us in this situation:

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

         +---+
 current | / |
         +---+
Some people think that we could then execute this line of code to complete the task:

        current = new ListNode(value);
But that won't work. It leaves us in this situation:

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

                    +------+------+
         +---+      | data | next |
 current | +-+--->  |  17  |   /  |
         +---+      +------+------+
This allocates a new node, but this new node has no connection to the original list. The list is still composed of 3 nodes linked together. This fourth node has been constructed, but hasn't been properly linked into the list.

As an 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."

As you learn about linked list programming, you'll find that there are only two ways to 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(value);
The code above will correctly append a value to the end of the list.

We were trying to write code for the appending add. So this code would be included inside a method and we would have to alter it to use the name of the parameter:

        public void add(int value) {
            ListNode current = front;
            while (current.next != null) {
                current = current.next;
            }
            current.next = new ListNode(value);
        }
Even this code isn't quite correct because we have to deal with the special case where the list might be empty:

        public void add(int value) {
            if (front == null) {
                front = new ListNode(value);
            } else {
                ListNode current = front;
                while (current.next != null) {
                    current = current.next;
                }
                current.next = new ListNode(value);
            }
        }
I said that we would discuss this code in more detail in section as well as implementing other methods of the LinkedIntList class.


Stuart Reges
Last modified: Wed Oct 18 15:30:23 PDT 2023