Circular linked lists
Circular linked lists connect the last node back to the first, so there is no null end. This lesson covers singly and doubly circular lists, safe loop termination, a circular queue and the Josephus problem.
Learning goals
- Build and traverse a circular list without looping forever
- Implement a queue with one tail pointer
- Solve Josephus by unlinking nodes
Prerequisites
Singly circular
In a circular singly linked list the last node's next points to the first node instead of null. Keeping a reference to the tail (rather than the head) gives O(1) access to both ends: tail.next is the head. It suits round-robin scheduling and any structure that cycles.
The last arrow in the Josephus animation returns to the first node.
1class CircularList {2Node tail; // tail.next is the head3void addLast(int v) {4Node n = new Node(v);5if (tail == null) { n.next = n; }6else { n.next = tail.next; tail.next = n; }7tail = n;8}9void addFirst(int v) {10Node n = new Node(v);11if (tail == null) { n.next = n; tail = n; }12else { n.next = tail.next; tail.next = n; } // tail stays the same13}14}
Josephus problem on a circular linked list
Interactive · edit the inputlinked list · 7 nodes
elimination order
7 people stand in a circle (the last arrow points back to the first). Count 3, remove that person, and continue counting from the next one.
1static int josephus(int n, int k) { // people 1..n stand in a circle2Node first = buildCircle(n); // circular list: last.next = first3Node prev = lastNode(first), cur = first;4for (int left = n; left > 1; left--) {5for (int i = 1; i < k; i++) { prev = cur; cur = cur.next; } // count k people6prev.next = cur.next; // unlink the k-th person7cur = cur.next; // counting restarts after the removed person8}9return cur.val; // the only node left10}
Live state
n
7
k
3
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=7 k=3 | 7 people stand in a circle (the last arrow points back to the first). Count 3, remove that person, and continue counting from the next one. |
| 2 | L5 | left=7 count=2 cur=2 | Count 2: person 2. |
| 3 | L5 | left=7 count=3 cur=3 | Count 3: person 3. |
| 4 | L6 | left=6 removed=3 | Person 3 is the 3-th: prev.next = cur.next unlinks them. 6 remain. |
| 5 | L5 | left=6 count=2 cur=5 | Count 2: person 5. |
| 6 | L5 | left=6 count=3 cur=6 | Count 3: person 6. |
| 7 | L6 | left=5 removed=6 | Person 6 is the 3-th: prev.next = cur.next unlinks them. 5 remain. |
| 8 | L5 | left=5 count=2 cur=1 | Count 2: person 1. |
| 9 | L5 | left=5 count=3 cur=2 | Count 3: person 2. |
| 10 | L6 | left=4 removed=2 | Person 2 is the 3-th: prev.next = cur.next unlinks them. 4 remain. |
| 11 | L5 | left=4 count=2 cur=5 | Count 2: person 5. |
| 12 | L5 | left=4 count=3 cur=7 | Count 3: person 7. |
| 13 | L6 | left=3 removed=7 | Person 7 is the 3-th: prev.next = cur.next unlinks them. 3 remain. |
| 14 | L5 | left=3 count=2 cur=4 | Count 2: person 4. |
| 15 | L5 | left=3 count=3 cur=5 | Count 3: person 5. |
| 16 | L6 | left=2 removed=5 | Person 5 is the 3-th: prev.next = cur.next unlinks them. 2 remain. |
| 17 | L5 | left=2 count=2 cur=4 | Count 2: person 4. |
| 18 | L5 | left=2 count=3 cur=1 | Count 3: person 1. |
| 19 | L6 | left=1 removed=1 | Person 1 is the 3-th: prev.next = cur.next unlinks them. 1 remain. |
| 20 | L9 | return=4 | Person 4 survives. Simulating with a circular list costs O(n·k); the recurrence J(n) = (J(n - 1) + k) mod n finds it in O(n). |
Doubly circular
A doubly circular list also links the first node's prev to the last node. With a sentinel node whose next and prev both point to itself when the ring is empty, no operation ever needs a null check. Rotating the ring is just moving one pointer.
Use it when you need to walk in either direction around a ring.
1class Ring {2final DNode sentinel = new DNode(0);3Ring() { sentinel.next = sentinel; sentinel.prev = sentinel; } // empty ring4void addLast(int v) {5DNode n = new DNode(v), last = sentinel.prev;6n.prev = last; n.next = sentinel;7last.next = n; sentinel.prev = n;8}9}
Loop termination
A loop over a circular list must not wait for null, because it never comes. Stop when you return to the starting node: use a do-while (visit first, then test), or count the nodes. Take care with the empty list, where there is no start node, and with removal during traversal, which can remove the start node itself.
The same idea detects that you have gone around once.
1static int sum(Node tail) { // tail.next is the head; null means empty2if (tail == null) return 0;3int total = 0;4Node cur = tail.next; // head5do {6total += cur.val;7cur = cur.next;8} while (cur != tail.next); // stop when we are back at the head9return total;10}
Circular queue implementation
A queue can be a circular singly linked list with one tail reference: enqueue inserts after the tail (the new node becomes the tail), and dequeue removes tail.next, the head. Both are O(1) and only one reference is stored. When the last node is removed the queue becomes empty (tail = null). An array-based circular buffer is the usual alternative.
Chapter 13 builds the array version.
1class LinkedQueue {2private Node tail; // null when empty3void enqueue(int v) {4Node n = new Node(v);5if (tail == null) n.next = n; else { n.next = tail.next; tail.next = n; }6tail = n;7}8int dequeue() {9if (tail == null) throw new NoSuchElementException();10Node head = tail.next;11if (head == tail) tail = null; else tail.next = head.next;12return head.val;13}14}
Josephus problem
In the Josephus problem n people stand in a circle and every k-th person is eliminated until one is left. With a circular list you count k nodes, unlink the k-th by relinking its predecessor, and continue from the next node: O(n·k) time. The recurrence J(1) = 0, J(n) = (J(n - 1) + k) mod n (zero-based) gives the survivor in O(n) without simulation.
Try n = 7, k = 3 and read the elimination order.
1static int josephusList(int n, int k) { // survivor, 1-based2Node first = new Node(1), cur = first;3for (int i = 2; i <= n; i++) { cur.next = new Node(i); cur = cur.next; }4cur.next = first; // close the circle5Node prev = cur; cur = first;6for (int left = n; left > 1; left--) {7for (int i = 1; i < k; i++) { prev = cur; cur = cur.next; }8prev.next = cur.next; // unlink the k-th person9cur = cur.next;10}11return cur.val;12}13static int josephusMath(int n, int k) { int r = 0; for (int i = 2; i <= n; i++) r = (r + k) % i; return r + 1; }
Josephus problem on a circular linked list
Interactive · edit the inputlinked list · 7 nodes
elimination order
7 people stand in a circle (the last arrow points back to the first). Count 3, remove that person, and continue counting from the next one.
1static int josephus(int n, int k) { // people 1..n stand in a circle2Node first = buildCircle(n); // circular list: last.next = first3Node prev = lastNode(first), cur = first;4for (int left = n; left > 1; left--) {5for (int i = 1; i < k; i++) { prev = cur; cur = cur.next; } // count k people6prev.next = cur.next; // unlink the k-th person7cur = cur.next; // counting restarts after the removed person8}9return cur.val; // the only node left10}
Live state
n
7
k
3
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=7 k=3 | 7 people stand in a circle (the last arrow points back to the first). Count 3, remove that person, and continue counting from the next one. |
| 2 | L5 | left=7 count=2 cur=2 | Count 2: person 2. |
| 3 | L5 | left=7 count=3 cur=3 | Count 3: person 3. |
| 4 | L6 | left=6 removed=3 | Person 3 is the 3-th: prev.next = cur.next unlinks them. 6 remain. |
| 5 | L5 | left=6 count=2 cur=5 | Count 2: person 5. |
| 6 | L5 | left=6 count=3 cur=6 | Count 3: person 6. |
| 7 | L6 | left=5 removed=6 | Person 6 is the 3-th: prev.next = cur.next unlinks them. 5 remain. |
| 8 | L5 | left=5 count=2 cur=1 | Count 2: person 1. |
| 9 | L5 | left=5 count=3 cur=2 | Count 3: person 2. |
| 10 | L6 | left=4 removed=2 | Person 2 is the 3-th: prev.next = cur.next unlinks them. 4 remain. |
| 11 | L5 | left=4 count=2 cur=5 | Count 2: person 5. |
| 12 | L5 | left=4 count=3 cur=7 | Count 3: person 7. |
| 13 | L6 | left=3 removed=7 | Person 7 is the 3-th: prev.next = cur.next unlinks them. 3 remain. |
| 14 | L5 | left=3 count=2 cur=4 | Count 2: person 4. |
| 15 | L5 | left=3 count=3 cur=5 | Count 3: person 5. |
| 16 | L6 | left=2 removed=5 | Person 5 is the 3-th: prev.next = cur.next unlinks them. 2 remain. |
| 17 | L5 | left=2 count=2 cur=4 | Count 2: person 4. |
| 18 | L5 | left=2 count=3 cur=1 | Count 3: person 1. |
| 19 | L6 | left=1 removed=1 | Person 1 is the 3-th: prev.next = cur.next unlinks them. 1 remain. |
| 20 | L9 | return=4 | Person 4 survives. Simulating with a circular list costs O(n·k); the recurrence J(n) = (J(n - 1) + k) mod n finds it in O(n). |
Practice ladder
Now try it yourself
Easy
- How do you detect that you have gone around a circular list once?
- What does an empty circular list with a tail reference look like?
Medium
- Find the survivor for n = 5, k = 2 by hand
- Implement dequeue for the circular linked queue
Advanced
- Prove the Josephus recurrence J(n) = (J(n - 1) + k) mod n
Key takeaways
- A tail reference gives O(1) access to both ends of a circular list.
- Never loop until null; stop when you return to the start.
- Josephus: unlink every k-th node, or use the O(n) recurrence.