Doubly linked, sorted and circular lists
The doubly linked list with a prev reference, the ordered list that keeps its elements sorted, and the circular list whose end is linked back to the start. The exam asks for reference update traces and the insertSorted method.
Last week we saw two weak points of the singly linked list: deleting from the end is O(n), and deleting a node requires searching for the one before it. This week there are three variations. The doubly linked list adds a prev reference to each node and can go backward. The ordered list always keeps its elements from smallest to largest. The circular list links the last node back to the start. Exam questions are again tracing and method writing; this time there are more references to update, so there is more room for mistakes.
Golden rules:
- In a doubly linked list every link is two-sided. If you did a.next = b, you must also do b.prev = a.
- head and tail are candidates in every operation. Inserting into an empty list and deleting the last element change both.
- Sorted insertion = find the right place + a normal insertion.
- There is no
nullin a circular list. End the loop with “have I come back to the start”.
The doubly linked list
class DNode {
int data;
DNode prev, next;
DNode(int d) { data = d; }
}
class DoublyLinkedList {
DNode head, tail;
int size;
}
head tail
| |
v v
[null|5|*] <-> [*|10|*] <-> [*|20|null]
Each node knows both directions. The cost: one extra reference of memory per node and twice as many reference updates in every operation.
Insertion
void addFirst(int x) {
DNode n = new DNode(x);
if (head == null) { head = tail = n; }
else { n.next = head; head.prev = n; head = n; }
size++;
}
void addLast(int x) {
DNode n = new DNode(x);
if (tail == null) { head = tail = n; }
else { n.prev = tail; tail.next = n; tail = n; }
size++;
}
void insertAfter(DNode p, int x) {
if (p == tail) { addLast(x); return; }
DNode n = new DNode(x);
n.prev = p; // 1
n.next = p.next; // 2
p.next.prev = n; // 3 (the right neighbour points back to n)
p.next = n; // 4 (last: we were still using p.next)
size++;
}
Four references change when inserting in the middle. The order of 3 and 4 matters: if you do p.next = n first, p.next.prev now becomes the prev of n itself, and the right neighbour is not updated.
Deletion
If you already hold the node to be deleted, you don’t need to search for the one before it. This is the biggest advantage of a doubly linked list.
void removeNode(DNode p) {
if (p.prev == null) head = p.next; else p.prev.next = p.next;
if (p.next == null) tail = p.prev; else p.next.prev = p.prev;
size--;
}
int removeLast() {
if (tail == null) throw new RuntimeException("empty list");
int x = tail.data;
tail = tail.prev;
if (tail == null) head = null;
else tail.next = null;
size--;
return x;
}
removeLast is now O(1). It was O(n) in a singly linked list.
Worked example
d.addLast(10); d.addLast(20); d.addLast(40); d.addFirst(5);
d.insertAfter(d.find(20), 30);
d.printForward(); d.printBackward();
d.removeNode(d.find(20));
System.out.println("removeLast=" + d.removeLast());
d.printForward(); d.printBackward();
5 10 20 30 40
40 30 20 10 5
removeLast=40
5 10 30
30 10 5
TIP · Print in both directions. The best way to test a doubly linked list method is to print both forward and backward. If it looks right forward but comes out different backward, you forgot a prev. On the exam, check the trace in both directions too.
The ordered linked list
The elements are always kept in ascending order. The user doesn’t say where to insert; the list finds the right place itself.
void insertSorted(int x) {
Node n = new Node(x);
if (head == null || x < head.data) { // empty or the smallest → front
n.next = head;
head = n;
return;
}
Node p = head;
while (p.next != null && p.next.data <= x)
p = p.next; // stop just before the first node greater than x
n.next = p.next;
p.next = n;
}
Let’s insert 5, 2, 8, 2, 6 in this order and print after each step:
5
2 5
2 5 8
2 2 5 8
2 2 5 6 8
With <= an equal value goes after the existing ones; if you had written < it would go before them. Both give a sorted list, but it makes a difference if the question says “equal values go after”.
Searching in a sorted list
Since the list is sorted, you can stop the moment you pass the value you are looking for:
boolean contains(int x) {
for (Node p = head; p != null; p = p.next) {
if (p.data == x) return true;
if (p.data > x) return false; // early exit
}
return false;
}
When searching for 7 in the list 2 2 5 6 8, 5 nodes are looked at (it stops at 8), and when searching for 1, a single node is looked at (2 > 1). The average case improves but the worst case is still O(n). You can’t do binary search on a linked list, because you can’t jump to the middle element in O(1).
| Operation | Unsorted list | Sorted list |
|---|---|---|
| Insertion | O(1) at the front | O(n) finding the place |
| Search (not found) | O(n), always to the end | O(n), but can stop early |
| Smallest element | O(n) | O(1), head |
The circular linked list
The next of the last node points to the first node instead of null. A practical trick: keep only the tail. The first node is always tail.next. That way you have O(1) access to both the front and the end.
+-----------------------------+
v |
tail.next=[0] -> [1] -> [2] -> [3]=tail-+
void addFirst(int x) {
Node n = new Node(x);
if (tail == null) { tail = n; n.next = n; } // a single node points to itself
else { n.next = tail.next; tail.next = n; }
}
void addLast(int x) { addFirst(x); tail = tail.next; } // add at the front, move the tail
int removeFirst() {
Node first = tail.next;
if (first == tail) tail = null; // there was a single element
else tail.next = first.next;
return first.data;
}
void rotate() { if (tail != null) tail = tail.next; } // the first element moves to the end
void print() {
Node p = tail.next;
do {
System.out.print(p.data + " ");
p = p.next;
} while (p != tail.next);
System.out.println();
}
Note the addLast trick: adding at the front and moving the tail one step forward makes the new node the last. A worked example:
c.addLast(1); c.addLast(2); c.addLast(3); c.addFirst(0);
c.print();
c.rotate(); c.print();
System.out.println("removeFirst=" + c.removeFirst());
c.print();
0 1 2 3
1 2 3 0
removeFirst=1
2 3 0
PITFALL ·
while (p != null)is an infinite loop in a circular list. No node’s next is null. Usedo-whileand stop when you come back to the start. If you start withwhile (p != tail.next)the loop never runs, because p already starts at tail.next.
Typical uses of a circular list: jobs that take turns (a round-robin scheduler), turns in a multiplayer game, and the Josephus problem (eliminating every k-th person in a circle).
Common mistakes
| Wrong | Correct |
|---|---|
| Updating only next in a doubly linked list | Set up every link in both directions: next and prev |
| Setting only head when inserting into an empty list | head = tail = n |
| Leaving the tail alone when deleting the last element | If the list became empty, head and tail are both null |
p.prev.next = p.next works on the head too | If p is the head, p.prev is null: check first |
| Forgetting the insert-at-front case in insertSorted | If x < head.data, it is the new head |
| Binary search on a sorted linked list | No O(1) access to the middle, the search is O(n) |
while (p != null) in a circular list | do { } while (p != tail.next) |
n.next = null in a single-element circular list | A single node points to itself: n.next = n |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
Starting from an empty doubly linked list, what do printForward() and printBackward() show?
list.addFirst(2);
list.addLast(4);
list.addFirst(1);
list.addLast(6);
list.removeFirst();
list.addFirst(8);Show answer
[2] → [2,4] → [1,2,4] → [1,2,4,6] → removeFirst: [2,4,6] → addFirst(8): [8,2,4,6]. Backward, from the tail via prev: 6 4 2 8.
When a new node is inserted between two existing nodes of a doubly linked list, how many references must be set or changed?
Show answer
The new node's prev and next (2 of them), the next of the left node and the prev of the right node (2 of them). Total 4. In a singly linked list this number is 2.
The doubly linked list is 1 2 3 4. What is printed?
DNode p = head.next;
System.out.println(p.prev.data + p.next.next.data);
System.out.println(tail.prev.prev.data);
p.next.data = p.prev.data * 10;
printForward();Show answer
p = 2. p.prev = 1, p.next.next = 4; int addition: 5. tail.prev.prev: 4 → 3 → 2. Finally the data of p.next (3) becomes 1 · 10 = 10: 1 2 10 4.
Write int removeLast() for a doubly linked list with head and tail. It must run in O(1) and handle a one-element list.
Show answer
In a doubly linked list, tail.prev gives the node before the last, so there is no need to walk.
int removeLast() {
if (tail == null) throw new RuntimeException("empty list");
int x = tail.data;
tail = tail.prev;
if (tail == null) // there was a single element
head = null;
else
tail.next = null; // the new last node
size--;
return x;
}For the list 7 8 9: two calls return 9 and 8, and the list stays 7. The third call returns 7, and both head and tail become null.Write void insertSorted(int x) for a singly linked list kept in ascending order. Equal values go after existing ones.
Show answer
Inserting at the front is a separate case: if the list is empty or x is the smallest. Otherwise stop just before the first node greater than x.
void insertSorted(int x) {
Node n = new Node(x);
if (head == null || x < head.data) {
n.next = head;
head = n;
return;
}
Node p = head;
while (p.next != null && p.next.data <= x)
p = p.next;
n.next = p.next;
p.next = n;
}Because <= is used, equal values come after the existing ones. If 5, 2, 8, 2, 6 are inserted in order, the list becomes 2 2 5 6 8.Values are inserted with insertSorted in this order: 9, 1, 8, 3, 5. What does the list print?
Show answer
[9] → 1 goes to the front: [1,9] → 8 goes in the middle: [1,8,9] → 3: [1,3,8,9] → 5: [1,3,5,8,9]. Whatever the insertion order, the list stays sorted.
The sorted list is 1 3 5 8 9. The contains method stops as soon as it sees a value greater than the target. How many nodes are examined when searching for 7?
Show answer
1, 3, 5 are smaller, continue. 8 > 7, so 7 can't be further ahead, return false. 1, 3, 5, 8: four nodes. In an unsorted list all 5 nodes would have to be looked at. The worst case is still O(n).
A circular singly linked list keeps only tail (tail.next is the first node). Starting empty, what is printed?
c.addLast(4);
c.addFirst(7);
c.addLast(9);
c.rotate(); // tail = tail.next
c.addFirst(1);
c.removeFirst();
c.rotate();
c.print(); // from tail.next around to tailShow answer
[4] → [7,4] → [7,4,9] (tail 9). rotate: tail becomes 7, reading from the start gives 4 9 7. addFirst(1): [1,4,9,7]. removeFirst removes 1: [4,9,7]. rotate: tail 4, reading gives 9 7 4.
Why does this loop never end on a circular linked list, and how should it be written?
Node p = tail.next;
while (p != null) {
System.out.print(p.data + " ");
p = p.next;
}Show answer
In a circular list the last node points to the first node, and no next is null. The stopping condition is coming back to the start: Node p = tail.next; do { print; p = p.next; } while (p != tail.next); The do-while prints once even in a single-element list.
People 1..6 sit in a circle (circular list, tail = 6). Repeat 5 times: rotate() then removeFirst() (remove every second person). In what order are people removed, and who survives?
Show answer
rotate moves the tail to 1, and the first node, 2, is removed. Then the tail goes to 3, and 4 is removed. The tail goes to 5, and 6 is removed. 1 3 5 remain: the tail goes to 1, and 3 is removed. 5 1 remain: the tail goes to 5, and 1 is removed. The survivor is 5. This is the Josephus problem, the classic application of a circular list.
A student removes node p from a doubly linked list with the two lines below. On which nodes does this crash, and how is it fixed?
p.prev.next = p.next;
p.next.prev = p.prev;Show answer
If p is the head, p.prev is null; if it is the tail, p.next is null; in both cases a NullPointerException. The fix:
if (p.prev == null) head = p.next;
else p.prev.next = p.next;
if (p.next == null) tail = p.prev;
else p.next.prev = p.prev;Name one operation that is O(n) in a singly linked list with head and tail, but O(1) in a doubly linked list with head and tail.
Show answer
In a singly linked list, finding the node before the last requires walking from the start: O(n). In a doubly linked list tail.prev is ready: O(1). Likewise, deleting a node you already hold a reference to is O(1) in a doubly linked list.
Verification: All traces and methods were run on the JDK in verify/SEN2211/W03.java with the DoublyLinkedList, SortedList and CircularList classes (java W03.java). Comparison counts were taken from the counter in the program.