Linked Lists I

Table of Contents

Adapted from Stuart Reges:

1 Reading

Read Bailey Chapter 9 through section 9.2 (p. 195–202 of the pdf) for an introduction to linked lists and a couple of example use cases.

2 Video

Here is a video lecture for the material outlined below.

The video contains sections on

  1. Motivation (0:06)
  2. From an array to a chain of nodes (2:10)
  3. Linked list node (5:15)
  4. Traversing a linked list (12:57)

The Panopto viewer has table of contents entries for these sections: https://carleton.hosted.panopto.com/Panopto/Pages/Viewer.aspx?id=b0f1f7a4-0b7c-483c-a8e4-acaa01691bc7

3 Motivation

  • Extensible arrays may be cool, but they are not perfect
    • Consider an inventory program:
    • Might be used to keep track of millions of items
    • Extending an ArrayList could be a slow operation due to all the copying
      • Even though it averages out to be constant time, an individual operation causing an extension will take linear time
    • Furthermore, our ArrayList is not particularly efficient for adding and removing elements from the beginning or middle, as those operations require shifting array elements over
    • If our inventory program uses large lists where items are frequently removed from the start of the list, extensible arrays would be a poor choice
  • In this topic we develop the concept of a linked list
    • A linked list is a dynamic structure that grows and shrinks exactly when necessary and whose elements may be added and removed in constant time regardless of position
    • In data structures, everything is a trade-off, so there is a cost to this dynamic behavior
      • lose constant-time random access (linked lists are sequential access structures)

4 Concept

With arrays, we might store a list of 6 ints as follows:

                 [0]   [1]   [2]   [3]   [4]   [5]
     +---+     +-----+-----+-----+-----+-----+-----+
list | +-+-->  |  0  |  2  |  40 |  23 |  14 |  72 |
     +---+     +-----+-----+-----+-----+-----+-----+

These values are contiguous in memory, meaning they are stored at adjacent locations.1 Imagine cutting this array up into individual variables and scattering them throughout memory:

+-----+   +-----+   +-----+   +-----+   +-----+   +-----+
|  23 |   |  2  |   |  40 |   |  0  |   |  14 |   |  72 |
+-----+   +-----+   +-----+   +-----+   +-----+   +-----+

If the values are going to be scattered throughout memory, we would have to somehow connect them to each other to keep track of the order of our list:

   +---->---->---->---->---->---->---->----+
   ^                                       |
   |         +----<----<----<----+         V
   ^         |                   ^         |
   |         V                   |         V
+-----+   +-----+   +-----+   +-----+   +-----+   +-----+
|  23 |   |  2  |-->|  40 |   |  0  |   |  14 |-->|  72 |-->end
+-----+   +-----+   +-----+   +-----+   +-----+   +-----+
   ^                   |         ^
   |                   V         |
   +----<----<----<----+       start

Each piece of our data refers (i.e., has a reference to) the next piece of data, and the final piece (72) has a special value to indicate it's the last piece. You might think that even with this interconnected structure, we'd have to keep track of where each value is stored. In fact, we just need a reference to the front of the list (start). So if we can get to the piece that stores 0 in it, then from there we can get to every other part of our list. This is the basic idea that we are going to explore with linked lists.

5 A Linked List is Made of Nodes

Linked lists are composed of individual elements called nodes. Each node is like a Lego building block. It looks unimpressive by itself, but once you put a bunch of them together, it can form an interesting structure.

A basic list node looks like this:

+------+------+
| data | next |
|  18  |  +---+--->
+------+------+

It's an object with two data fields: one for storing a single item of data and one for storing a reference to the next node in the list. For a list of int values, we'd declare this as follows:

class ListNode {
    int data;
    ListNode next;
}

This is our first example (but hardly the last) of a recursive data structure (a class that is defined in terms of itself in that ListNode has a data field of type ListNode). For this to make sense, we have to remember that in Java object variables are references, meaning they store the memory address of the object. So a ListNode doesn't literally have another ListNode inside of it (this would give us infinitely nesting nodes), but rather a reference to the next node in the list.

Let's look at how we could use the ListNode class to build up the list (3, 7, 12). Obviously we're going to need three nodes that are linked together. With linked lists, if you have a reference to the front of the list, then you can get to anything in the list. So we'll usually have a single variable of type ListNode that refers to (or points to) the front of the list. So we began with this declaration:

ListNode list;

The variable list is not itself a node. It's a variable that is capable of referring to a node. So we'd draw it something like this:

     +---+
list | ? |
     +---+

where we understand that the "?" is going to be replaced with a reference to a node. So this box does not have a data field or a next field. It's a box where we can store a reference to such an object.

We don't have an actual node until we use new to construct one:

list = new ListNode();

This constructs a new node and tells Java to have the variable list refer to it:

                +------+------+
     +---+      | data | next |
list | +-+--->  |      |      |
     +---+      +------+------+

What do we want to do with this node? We want to store 3 in its data field (list.data) and we want its next field to point to a new node:

list.data = 3;
list.next = new ListNode();

which leads us to this situation:

                +------+------+      +------+------+
     +---+      | data | next |      | data | next |
list | +-+--->  |   3  |   +--+--->  |      |      |
     +---+      +------+------+      +------+------+

When you program linked lists, you have to be careful to keep track of what you're talking about. The variable list stores a reference to the first node. We can get inside that node with the dot notation (list.data and list.next). So list.next is the way to refer to the next box of the first node. We wrote code to assign it to refer to a new node, which is why list.next is pointing at this second node. Now we want to assign the second node's data field (list.next.data) to the value 7 and assign the second node's next field to refer to a third node:

list.next.data = 7;
list.next.next = new ListNode();

which leads us to this situation:

                +------+------+      +------+------+      +------+------+
     +---+      | data | next |      | data | next |      | data | next |
list | +-+--->  |   3  |   +--+--->  |   7  |   +--+--->  |      |      |
     +---+      +------+------+      +------+------+      +------+------+

We need to pay close attention to list versus list.next versus list.next.next and remember which box each of those coincides with:

                +------+------+      +------+------+      +------+------+
     +---+      | data | next |      | data | next |      | data | next |
list | +-+--->  |   3  |   +--+--->  |   7  |   +--+--->  |      |      |
     +---+      +------+------+      +------+------+      +------+------+
       |                   |                    |
       |                   |                    |
      list             list.next          list.next.next

Finally, we want to set the data field of this third node to 12 (list.next.next.data) and we want to set its next field to null. The keyword null is a Java word that means no object. This provides a terminator for the linked list (a special value that indicates that we are at the end of the list). So we'd execute these statements:

list.next.next.data = 12;
list.next.next.next = null;

which leaves us in this situation:

                +------+------+      +------+------+      +------+------+
     +---+      | data | next |      | data | next |      | data | next |
list | +-+--->  |   3  |   +--+--->  |   7  |   +--+--->  |  12  |   /  |
     +---+      +------+------+      +------+------+      +------+------+

We draw a diagonal line through the last next field as a way to indicate that it's value is null. The assignment to null is actually unnecessary. Java will initialize all data fields to the zero equivalent for that particular type. For type int, that means initializing to 0. For double, it initializes to 0.0. For boolean, it initializes to false. For arrays and other objects, it initializes to null. But it's not a bad idea to include the code to make it perfectly clear what's going on (and not all programming languages do this—C, for example, initializes nothing—so always initializing variables is a good habit to cultivate).

6 Traversing a Linked List

Obviously this is a very tedious way to manipulate a list. It's much better to write code that involves loops to manipulate lists. Suppose we have a variable fibs that stores a reference to the list (1, 1, 2, 3, 5):

              +------+------+    +------+------+    +------+------+    +------+------+    +------+------+ 
     +---+    | data | next |    | data | next |    | data | next |    | data | next |    | data | next |
fibs | +-+--> |   1  |   +--+--> |   1  |   +--+--> |   2  |   +--+--> |   3  |   +--+--> |   5  |   /  |
     +---+    +------+------+    +------+------+    +------+------+    +------+------+    +------+------+

Using the same techniques as above, we could access the data fields with list.data, list.next.data, list.next.next.data, list.next.next.next.data, and list.next.next.next.next.data (clearly gross, and infeasible for a list of more than a handful of elements). So let's write a loop to traverse the list.

We have just one variable to work with, so that's clearly where we have to start (the variable fibs). 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 = fibs;

This initializes current to point to the same value as fibs (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 |    | data | next |    | data | next |
fibs | +-+--> |   1  |   +--+--> |   1  |   +--+--> |   2  |   +--+--> |   3  |   +--+--> |   5  |   /  |
     +---+    +------+------+    +------+------+    +------+------+    +------+------+    +------+------+
                     ^     
        +---+        |     
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 = fibs;
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 reference to the next node is stored in current.next, so moving to that next node involves resetting current to current.next:

ListNode current = fibs;
while (current != null) {
    System.out.println(current.data);
    current = current.next;
}

The first time through this loop, current is referring to the first node with a 1 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 |    | data | next |    | data | next |
fibs | +-+--> |   1  |   +--+--> |   1  |   +--+--> |   2  |   +--+--> |   3  |   +--+--> |   5  |   /  |
     +---+    +------+------+    +------+------+    +------+------+    +------+------+    +------+------+
                                        ^
        +---+                           |
current | +-+------>------>------>------+
        +---+

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 also 1), and move current along again:

              +------+------+    +------+------+    +------+------+    +------+------+    +------+------+ 
     +---+    | data | next |    | data | next |    | data | next |    | data | next |    | data | next |
fibs | +-+--> |   1  |   +--+--> |   1  |   +--+--> |   2  |   +--+--> |   3  |   +--+--> |   5  |   /  |
     +---+    +------+------+    +------+------+    +------+------+    +------+------+    +------+------+
                                                           ^
        +---+                                              |
current | +-+------>------>------>------>------>------>----+
        +---+

Once again current is not null, so we go into the loop a third and a fourth, printing the value of current.data (2 then 3) and resetting current each time. This leaves us here:

              +------+------+    +------+------+    +------+------+    +------+------+    +------+------+ 
     +---+    | data | next |    | data | next |    | data | next |    | data | next |    | data | next |
fibs | +-+--> |   1  |   +--+--> |   1  |   +--+--> |   2  |   +--+--> |   3  |   +--+--> |   5  |   /  |
     +---+    +------+------+    +------+------+    +------+------+    +------+------+    +------+------+
                                                                                                 ^
        +---+                                                                                    |
current | +-+------>------>------>------>------>------>------>------>------>------>------>------>+
        +---+

On this iteration, 5 gets printed when we print current.data, but this time current.next has the value null. So when we reset current we get:

              +------+------+    +------+------+    +------+------+    +------+------+    +------+------+ 
     +---+    | data | next |    | data | next |    | data | next |    | data | next |    | data | next |
fibs | +-+--> |   1  |   +--+--> |   1  |   +--+--> |   2  |   +--+--> |   3  |   +--+--> |   5  |   /  |
     +---+    +------+------+    +------+------+    +------+------+    +------+------+    +------+------+

        +---+
current | / |
        +---+

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

1
1
2
3
5

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 = fibs; current != null; current = current.next) {
    System.out.println(current.data);
}

It's a matter of personal taste which kind of loop to use.

7 Practice Problems2

Use this definition of a ListNode for these problems.

class ListNode {
    private int data;
    private ListNode next;

    ListNode(int data, ListNode next) {
        this.data = data;
        this.next = next;
    }
}
                 +------+------+      +------+------+      +------+------+
      +---+      | data | next |      | data | next |      | data | next |
front | +-+--->  |   2  |   +--+--->  |   5  |   +--+--->  |  12  |   /  |
      +---+      +------+------+      +------+------+      +------+------+
  1. Write code to insert a new node value 10 at the beginning of the list drawn above.
  2. Write code to insert a new value 10 at the end of the list drawn above. Use a loop to get a reference to the last node.
  3. For each of the eight problems below, you'll see pictures of linked nodes before and after changes. Write the code that will produce the given result by modifying links between the nodes shown and/or creating new nodes as needed. There may be more than one way to write the code, but you may not change any existing node's data field value. If a variable does not appear in the "after" picture, it doesn't matter what value it has after the changes are made.

    linked-list-before-after.png

8 Learning Block

  • Lab 0 Gallery
  • Questions?
  • Clarifications
    • All materials linked from the course webpage are always allowed (for quizzes and the final take-home exam)
    • You are free to use any code provided on the course web page without needing to cite it
    • Googling things about Java syntax is highly encouraged!
      • You can search for things like "Java loop over array" or "Java read in numbers" or the compiler error you're getting
      • Do not search for things like "Silver dollar game in Java" or "printing out CoinStrip game board" as these are not Java syntax, but instead your specific tasks on the lab
    • Come to office hours/post in our Discord channel/send me email about the lab!
  • Form question
  • Fill in table for array code and linked list code when loop over elements
    • go to front of list (int i = 0, ListNode current = list)
    • test for more elements (i < array.length, current != null)
    • current value (array[i], current.data)
    • go to next element (i++, current = current.next)
  • Work on lab 1

Footnotes:

1

Recall that our notional machine defines memory as a long row of slots, each of which can store a piece of information. An array is stored in a range of adjacent slots.

2

Solutions:

  1. front = new ListNode(10, front);
    
  2. ListNode current = front;
    while (current.next != null) {
        current = current.next
    }
    current.next = new ListNode(10, current.next);  // could use null instead of current.next as argument
    
    • 9.
      list.next.next = new ListNode(3, null);   // 2 -> 3
      
    • 10.
      list = new ListNode(3, list);   // 3 -> 1  and  list -> 3
      
    • 11.
      temp.next.next = list.next;   // 4 -> 2
      list.next = temp;             // 1 -> 3
      
    • 12.
      ListNode list2 = list;          // list2 -> 1
      list = list.next;               // list -> 2
      list2.next = list2.next.next;   // 1 -> 3
      list.next = null;               // 2 /
      
    • 13.
      ListNode temp = list;    // temp -> 5
      list = list.next;        // list -> 4
      temp.next = list.next;   // 5 -> 3
      list.next = temp;        // 4 -> 5
      
    • 14.
      ListNode temp = list.next.next;   // temp -> 3
      temp.next = list.next;            // 3 -> 4
      list.next.next = list;            // 4 -> 5
      list.next.next.next = null;       // 5 /
      list = temp;                      // list -> 3
      
    • 15.
      list.next.next.next = temp;        // 3 -> 4
      temp.next.next = list.next.next;   // 5 -> 3
      list.next.next = null;             // 2 /
      ListNode temp2 = temp.next;        // temp2 -> 5
      temp.next = list.next;             // 4 -> 2
      list = temp2;                      // list -> 5
      
    • 16.
      list2.next.next.next = list;   // 4 -> 1
      list.next = list2;             // 1 -> 2
      list = list2.next.next;        // list -> 4
      list2 = list2.next;            // list2 -> 3
      list2.next = null;             // 3 /
      list.next.next.next = null;    // 2 /