Linked Lists I
Table of Contents
Adapted from Stuart Reges:
- https://courses.cs.washington.edu/courses/cse143/20wi/notes/notes06.html
- https://courses.cs.washington.edu/courses/cse143/20wi/notes/notes07.html
- https://courses.cs.washington.edu/courses/cse143/20wi/notes/notes08.html
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 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)
3 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.
4 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).
5 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.
6 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 | / | +---+ +------+------+ +------+------+ +------+------+
- Write code to insert a new node value 10 at the beginning of the list drawn above.
- 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.
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.
Footnotes:
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.
Solutions:
front = new ListNode(10, front);
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 /