Handling boundary conditions (head == null, inserting at index 0, removing the last element) often leads to messy code cluttered with if-else edge checks. Two elegant techniques solve these issues: Circular Lists and Sentinel (Dummy) Nodes.
Circular Linked Lists
In a standard linked list, the tail node's next pointer points to null.
In a Circular Linked List, the tail's next pointer loops directly back to the head node!
+---------------------------------------+
| |
v |
[ 10 | * ] ---> [ 20 | * ] ---> [ 30 | * ]
Head Tail
Why Circular Lists?
- Continuous Round-Robin Cycling: Any node can serve as a starting point for full list traversal.
O(1)Access to Head and Tail: If you maintain a single pointer totail:tailgives you the last element inO(1).tail.nextgives you theheadinO(1)! You don't even need a separateheadpointer!
Real-World Use Cases:
- Operating System Task Schedulers: Round-robin CPU time-slice distribution across threads.
- Media Players: Repeat playlist feature that cycles continuously without re-initialization.
- Multiplayer Turn-Based Games: Cycling through player turns (Player 1 -> Player 2 -> Player 3 -> Player 1).
Sentinel (Dummy) Nodes
A Sentinel Node is a dummy node that holds no meaningful data and exists solely to anchor the head (and optionally tail) of a linked list.
Without Sentinel:
Empty list: head = null
Add first item: Must write special if (head == null) { head = newNode }
With Sentinel:
Empty list: dummyHead ---> null
List with 1 item: dummyHead ---> [ Data: 10 | null ]
Eliminating Edge Cases in Code
Observe how sentinel nodes eliminate special-case if (prev == null) checks:
// Removing a node with value 'x' WITHOUT sentinel
fun removeValue(head: Node<Int>?, target: Int): Node<Int>? {
var h = head
// Special case 1: target is at head
while (h != null && h.data == target) {
h = h.next
}
var curr = h
// Standard case: target in middle or tail
while (curr?.next != null) {
if (curr.next!!.data == target) {
curr.next = curr.next!!.next
} else {
curr = curr.next
}
}
return h
}
// Removing a node with value 'x' WITH Sentinel Node
fun removeValueClean(head: Node<Int>?, target: Int): Node<Int>? {
val sentinel = Node(0, head) // Anchor dummy before head
var curr = sentinel
while (curr.next != null) {
if (curr.next!!.data == target) {
curr.next = curr.next!!.next // Universal deletion!
} else {
curr = curr.next!!
}
}
return sentinel.next // Clean new head
}
Doubly Linked List with Head and Tail Sentinels
The gold standard implementation of a Doubly Linked List uses two sentinels: head and tail.
[ DUMMY HEAD ] <=======> [ Real Node A ] <=======> [ Real Node B ] <=======> [ DUMMY TAIL ]
Every real node is guaranteed to have both a valid prev and a valid next. Pointers never dereference null, eliminating NullPointerException errors entirely!
Summary
- Circular lists connect tail to head, ideal for round-robin scheduling and infinite playlists.
- Sentinel nodes eliminate edge-case
nullchecks during insertions and deletions, producing cleaner, bug-free linked list operations.