CSE143 Notes for Wednesday, 10/12/05

I used handout #8 to review what we had discussed on Monday, that we have a class called ListNode that has data fields for "data" and "next" and that has several constructors:
        public class ListNode {
            public int data;
            public ListNode next;
        
            public ListNode() {
                this(0, null);
            }
        
            public ListNode(int data) {
                this(data, null);
            }
        
          public ListNode(int data, ListNode next) {
              this.data = data;
              this.next = next;
          }
        }
As with the other classes we've seen, there is one "real" constructor (the one that takes two arguments). The other two use the "this(...)" notation to call the third constructor with default values (0 for the data, null for next).

I mentioned that because linked lists can be confusing, people have come up with many exercises they perform in lecture to help explain this to people. I even have a friend who brought in a tank of helium to string together helium balloons together to help explain linked lists to his students. I tend to think that most of these don't help much, but I thought I'd try one that might help explain what's going on.

I mentioned that Java has two sections of memory where it allocates variables. The first is known as the "stack" or the "call stack" and that is where all local variables are allocated. So when you say something like:

        int x;
or:
        ListNode p;
Those variables (x and p) are allocated on the stack. We'll be talking more about the stack later in cse143. The stack is very well behaved in that when we exit a method, we can take all of its local variables off the top of the stack (something we call "popping" the stack).

The other section of memory is known as the "heap" and it is less organized than the stack. All objects in a Java program reside in the heap. Another way of saying this is that all objects in Java are heap-allocated. C and C++ allow you to have stack-allocated objects, but not Java.

To help explain this, I put up an overhead that had room for 16 list node objects and I put an index next to each box that I said you could think of as an address in memory.

        Address  data  next
                +-----+-----+
              0 |     |     |
                +-----+-----+
              1 |     |     |
                +-----+-----+
              2 |     |     |
                +-----+-----+
              3 |     |     |
                +-----+-----+
              4 |     |     |
                +-----+-----+
              5 |     |     |
                +-----+-----+
              6 |     |     |
                +-----+-----+
              7 |     |     |
                +-----+-----+
              8 |     |     |
                +-----+-----+
              9 |     |     |
                +-----+-----+
             10 |     |     |
                +-----+-----+
             11 |     |     |
                +-----+-----+
             12 |     |     |
                +-----+-----+
             13 |     |     |
                +-----+-----+
             14 |     |     |
                +-----+-----+
             15 |     |     |
                +-----+-----+
The actual heap has much more room than this, but this is a useful way to think of the heap. I asked people to name some numbers between 0 and 15 and people mentioned them in the following order: 9, 2, 15, 5, 7, 14, 8, 5. I said that as we allocate nodes from the heap, we'll do so in this order. The point is that objects can be located anywhere in the heap and the program will still work.

I introduced a variable called "list" that will store the reference to the front of the list. This is a local variable that would be stack allocated. I put a big box off to the side to represent list:

             +-----+
        list |     |
             +-----+
I then said to consider what happens when we say:

        list = new ListNode(62);
First Java has to evaluate the call on "new" by allocating a new ListNode with 62 in its data field and null in its next field. According to the numbers people gave me, we decided to use location 9 for the first node in the heap, so we put the "62" and "null" into those data fields. I said we'd be storing all integers, so I used "-1" to represent null:

        Address  data  next
                +-----+-----+
              0 |     |     |
                +-----+-----+
              1 |     |     |
                +-----+-----+
              2 |     |     |
                +-----+-----+
              3 |     |     |
                +-----+-----+
              4 |     |     |
                +-----+-----+
              5 |     |     |
                +-----+-----+
              6 |     |     |
                +-----+-----+
              7 |     |     |
                +-----+-----+
              8 |     |     |
                +-----+-----+
              9 |  62 |  -1 |
                +-----+-----+
             10 |     |     |
                +-----+-----+
             11 |     |     |
                +-----+-----+
             12 |     |     |
                +-----+-----+
             13 |     |     |
                +-----+-----+
             14 |     |     |
                +-----+-----+
             15 |     |     |
                +-----+-----+
Because we stored this in location 9, the call on new returns 9 as its result and we store that in list:

             +-----+
        list |  9  |
             +-----+
Then I asked people to think about what happens when we execute this line of code:
        list.next = new ListNode(17);
First we execute the "new" part. We need something from the heap, and according to the numbers people gave me, we were supposed to use location 2 second, so we create this node there by storing 17 in the data field and null (-1) in the next field. That call on new returns the 2 which is the location of the new node and we store that in list.next. list has the value 9, which means we go to location 9, and list.next is the "next" box at that location, so we store a 2 in it:

        Address  data  next
                +-----+-----+
              0 |     |     |
                +-----+-----+
              1 |     |     |
                +-----+-----+
              2 |  17 |  -1 |
                +-----+-----+
              3 |     |     |
                +-----+-----+
              4 |     |     |
                +-----+-----+
              5 |     |     |
                +-----+-----+
              6 |     |     |
                +-----+-----+
              7 |     |     |
                +-----+-----+
              8 |     |     |
                +-----+-----+
              9 |  62 |   2 |
                +-----+-----+
             10 |     |     |
                +-----+-----+
             11 |     |     |
                +-----+-----+
             12 |     |     |
                +-----+-----+
             13 |     |     |
                +-----+-----+
             14 |     |     |
                +-----+-----+
             15 |     |     |
                +-----+-----+
I then asked people to consider what happens when we execute this line of code:

        list = new ListNode(106, list);
First we construct the node. Our list said to use location 15 next, so we go there and store 106 in the data field and the value of list (9) in the next field.

        Address  data  next
                +-----+-----+
              0 |     |     |
                +-----+-----+
              1 |     |     |
                +-----+-----+
              2 |  17 |  -1 |
                +-----+-----+
              3 |     |     |
                +-----+-----+
              4 |     |     |
                +-----+-----+
              5 |     |     |
                +-----+-----+
              6 |     |     |
                +-----+-----+
              7 |     |     |
                +-----+-----+
              8 |     |     |
                +-----+-----+
              9 |  62 |   2 |
                +-----+-----+
             10 |     |     |
                +-----+-----+
             11 |     |     |
                +-----+-----+
             12 |     |     |
                +-----+-----+
             13 |     |     |
                +-----+-----+
             14 |     |     |
                +-----+-----+
             15 | 106 |   9 |
                +-----+-----+
Then we return the location, which is 15, and this becomes the new value of list:

             +-----+
        list |  15 |
             +-----+
So now we have list pointing to location 15, location 15 has 106 in it and points to location 9, location 9 has 62 in it and points to location 2, location 2 has 17 in it and points to null. So we've built up the list (106, 62, 17).

The line of code we executed before is the "prepending" add (putting a new value at the front of the list). We tried one more example:

        list = new ListNode(-18, list);
According to our list, we were supposed to use location 5 next. So went to that location in the heap and stored a -18 in the data field and the current value of list (15) in the next field:

        Address  data  next
                +-----+-----+
              0 |     |     |
                +-----+-----+
              1 |     |     |
                +-----+-----+
              2 |  17 |  -1 |
                +-----+-----+
              3 |     |     |
                +-----+-----+
              4 |     |     |
                +-----+-----+
              5 | -18 |  15 |
                +-----+-----+
              6 |     |     |
                +-----+-----+
              7 |     |     |
                +-----+-----+
              8 |     |     |
                +-----+-----+
              9 |  62 |   2 |
                +-----+-----+
             10 |     |     |
                +-----+-----+
             11 |     |     |
                +-----+-----+
             12 |     |     |
                +-----+-----+
             13 |     |     |
                +-----+-----+
             14 |     |     |
                +-----+-----+
             15 | 106 |   9 |
                +-----+-----+
This call on new then returned the location it used (5) and this became the new value of list:

             +-----+
        list |  5  |
             +-----+
Someone then asked what the following line of code would do:

        list = new ListNode(210, list.next);
With this kind of simulation of the heap, it is easy to just do exactly what the line of code says to do. The next location on our list was 7, so we went to that location in the heap. There we stored data of 210 and for next we used the value of list.next, which was 15 (list points to location 5 and the value in the "next" field at that location is 15).

        Address  data  next
                +-----+-----+
              0 |     |     |
                +-----+-----+
              1 |     |     |
                +-----+-----+
              2 |  17 |  -1 |
                +-----+-----+
              3 |     |     |
                +-----+-----+
              4 |     |     |
                +-----+-----+
              5 | -18 |  15 |
                +-----+-----+
              6 |     |     |
                +-----+-----+
              7 | 210 |  15 |
                +-----+-----+
              8 |     |     |
                +-----+-----+
              9 |  62 |   2 |
                +-----+-----+
             10 |     |     |
                +-----+-----+
             11 |     |     |
                +-----+-----+
             12 |     |     |
                +-----+-----+
             13 |     |     |
                +-----+-----+
             14 |     |     |
                +-----+-----+
             15 | 106 |   9 |
                +-----+-----+
The call on new returns the location we just used (7) and this becomes the new value of list:

             +-----+
        list |  7  |
             +-----+
We then spent a moment figuring out what we had. Our variable list points to location 7, which has the value 210 and points to 15, which has the value 106 and points to 9, which has the value 62 and points to 2, which has the value 17 and points to null. In other words, we are storing the list (210, 106, 62, 17).

So what about the node stored in location 5? It is no longer part of the list. The garbage collector would discover that it is unreachable and would reuse the space if it needs it.

I also drew a before and after picture of this situation using our normal linked list pictures. Before this line of code executes, we have a list of four elements:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | +-+--->  |  -18 |   +--+--->  | 106  |   +--+--->  |  62  |   +  |
         +---+      +------+------+      +------+------+      +------+---+--+
                                                                         |
                                                                         V
                                                              +------+------+
                                                              | data | next |
                                                              |  17  |   /  |
                                                              +------+------+
We construct a new node that points to list.next (the node with 106 in it) and we make this the new value of list:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
    list | + |      |  -18 |   +--+--->  | 106  |   +--+--->  |  62  |   +  |
         +-+-+      +------+------+      +------+------+      +------+---+--+
           |                                    ^                        |
           |                                    |                        V
           V        +------+------+             |             +------+------+
           |        | data | next |             |             | data | next |
           +----->  | 210  |   +--+-->----------+             |  17  |   /  |
                    +------+------+                           +------+------+
As we saw with our heap picture, the node that has -18 in its data field is no longer part of the list and is no longer reachable.

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 of the IntList class. I pointed out that this is our first example of the concept of "data abstraction". We have spent a lot of time studying how to create a class called IntList whose instances each store a list of integers by keeping track of an array and a size. Now we are going to see how the same functionality can be built using 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> } So these are two fundamentally different ways to get the same kind of behavior. From the point of view of a client, these two classes accomplish the same thing and in some sense we don't care how each does its job as long as it does it correctly. But from the point of view of the implementor of the class, the details are quite different.

Then I turned to the question of how to write code that would print out the contents of one of these lists. We have just one variable to work with, so that's clearly where we have to start (the variable "front" or "this.front"). 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 = front;
This initializes current to point to the same value as front (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. Suppose that the list stores the values (3, 5, 2). Then after executing the statement above, we have the following situation:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   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 = front; 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 = front;
        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 |
   front | +-+--->  |   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 |
   front | +-+--->  |   3  |   +--+--->  |   5  |   +--+--->  |   2  |   /  |
         +---+      +------+------+      +------+------+      +------+------+
                           ^
                           |          
                         +-+-+
                 current | + |
                         +---+
And after executing the statement "current = current.next", we'd have this situation:

                    +------+------+      +------+------+      +------+------+
         +---+      | data | next |      | data | next |      | data | next |
   front | +-+--->  |   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 |
   front | +-+--->  |   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 |
   front | +-+--->  |   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:

        int i = 0;
        while (i < size) {
            System.out.println(elementData[i]);
            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 = front;
test for more elements i < size current != null
current value elementData[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 = first; 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.


Stuart Reges
Last modified: Fri Oct 14 21:37:27 PDT 2005