Doubly & Circular Linked Lists
Bidirectional pointer maintenance, sentinel heads/tails, O(1) arbitrary node deletion, circular ring buffers, and Josephus problem.
Topics Covered:
25. Doubly Linked List (DLL) Architecture & Bi-Directional Traversal • Arbitrary Deletion Mechanics • The Sentinel / Dummy Node Pattern • 26. Circular Singly Linked List (CSLL) • Circular Doubly Linked List (CDLL) • The Josephus Elimination Problem & Simulation
While singly linked lists provide unidirectional linear chains, systems programming and high-performance caching require bi-directional navigation and true arbitrary node removals. Doubly linked lists achieve this by embedding both predecessor (prev) and successor (next) references within each node, while circular variants loop terminal pointers back to the entry node to form endless ring buffers. This chapter details bidirectional heap node topologies, sentinel-based elimination of boundary pointer edge cases, the mechanics of constant-time arbitrary deletion, and cyclic simulations including the classic Josephus elimination problem.
Learning Objectives
#- Differentiate the memory layouts, pointer alignment overheads, and traversal capabilities of singly, doubly, and circular linked structures.
- Prove why holding a direct node reference enables true deletion in doubly linked lists versus in singly linked lists.
- Implement the Sentinel (Dummy Head & Tail) architectural pattern to eliminate null-pointer branch penalties and edge-case exceptions.
- Construct Circular Singly Linked Lists (CSLL) using a single
tailpointer to guarantee insertions at both ends without trackinghead. - Formulate the Josephus elimination problem using circular pointer chains and verify the simulation against the closed-form recurrence .
Topic 25: Doubly Linked Lists (DLL)
#1. Conceptual Architecture & Node Anatomy
A Doubly Linked List (DLL) is a sequence of dynamically allocated nodes where each node contains two pointers:
next: Stores the virtual heap address of the succeeding node.prev: Stores the virtual heap address of the preceding node.
┌─────────────────────────────────────────────────────────────────────────┐
│ 64-BIT DOUBLY NODE LAYOUT │
├───────────────────┬───────────────────┬─────────────────────────────────┤
│ prev pointer │ data payload │ next pointer │
│ (8 bytes) │ (4 bytes) │ (8 bytes) │
│ Offset: +0 │ Offset: +8 │ Offset: +16 │
└───────────────────┴───────────────────┴─────────────────────────────────┘| Field | Type | Size (64-bit Architecture) | Alignment / Offset | Architectural Function |
|---|---|---|---|---|
prev | Node Reference | Offset | Holds virtual heap address of immediate predecessor node | |
data | Value / Payload | Offset | Stores client data (e.g., 32-bit integer) | |
padding | System Alignment | Offset | Padding to preserve 8-byte boundary alignment | |
next | Node Reference | Offset | Holds virtual heap address of immediate successor node |
Total node size is . Compared to a flat array storing a 4-byte integer, a DLL node incurs a memory overhead.
Virtual Memory Layout Example
| Logical Position | Virtual Heap Address | prev Pointer | data Value | next Pointer | Semantic Role |
|---|---|---|---|---|---|
Node 1 (HEAD) | 0x10A0 | NULL (0x0) | 10 | 0x20F4 | First data node; no predecessor |
| Node 2 | 0x20F4 | 0x10A0 | 20 | 0x15C8 | Interior node; bidirectional links |
Node 3 (TAIL) | 0x15C8 | 0x20F4 | 30 | NULL (0x0) | Last data node; no successor |
2. Operations & Asymptotic Complexities
#| Operation | Best Case | Average Case | Worst Case | Auxiliary Space | Comparison vs. Singly Linked List |
|---|---|---|---|---|---|
| InsertAtHead() | Identical bound; requires updating oldHead.prev | ||||
| InsertAtTail() | Identical bound via tail reference | ||||
| DeleteHead() | Identical bound; sets new head.prev = NULL | ||||
| DeleteTail() | vs in SLL (immediate predecessor via tail.prev) | ||||
| DeleteNode() | arbitrary deletion without scanning from head | ||||
| InsertBeforeNode() | vs in SLL (direct access to ) | ||||
| InsertAfterNode() | constant time pointer insertion | ||||
| Search() | Linear scan; can traverse from head or tail | ||||
| ReverseInPlace() | Swap prev and next pointers on every node |
3. The Superpower of DLLs: Arbitrary Deletion
#In cache eviction systems (e.g., Least Recently Used / LRU Cache) and OS process scheduling queues, an algorithm frequently needs to evict an item given a direct reference to that node (for instance, retrieved from an auxiliary hash map).
- In a Singly Linked List, deleting node requires starting at
HEADand walking forward until finding node whosenext == X( time). - In a Doubly Linked List, node already knows its predecessor () and its successor (). Deletion requires only rewiring two pointers!
State Transition Table: Deleting Node from
| Step | Action Taken | Pointer Expression | Consequence |
|---|---|---|---|
| 1 | Point predecessor forward | target.prev.next = target.next | Node 10's next now skips Node 20 to point directly to Node 30 |
| 2 | Point successor backward | target.next.prev = target.prev | Node 30's prev now skips Node 20 to point directly to Node 10 |
| 3 | Deallocate target node | free(target) | Node 20 memory reclaimed; remains intact |
4. The Sentinel (Dummy) Node Architectural Pattern
#Managing boundary conditions in raw linked lists leads to conditional branches for empty lists, single-element lists, head mutations, and tail mutations.
The Sentinel Solution
Introduce two permanent invariant nodes that hold no client data:
headSentinel: Always sits before the first true data node.tailSentinel: Always sits after the last true data node.
Empty List with Sentinels:
[ headSentinel ] <===================> [ tailSentinel ]
(prev = NULL, next = tailSentinel) (prev = headSentinel, next = NULL)
Populated List with Sentinels:
[ headSentinel ] <===> [ Node 10 ] <===> [ Node 20 ] <===> [ Node 30 ] <===> [ tailSentinel ]Why Sentinels Eliminate Edge Cases:
Every user node—regardless of whether it is at the front, middle, or back—always has a non-null predecessor and a non-null successor. Special if (head == NULL) checks completely disappear from the codebase.
5. Production Specification: Sentinel Doubly Linked List
#CLASS DoublyNode:
field data: ValueType
field prev: DoublyNode Pointer <- NULL
field next: DoublyNode Pointer <- NULL
CONSTRUCTOR(val: ValueType):
this.data <- val
CLASS SentinelDoublyLinkedList:
field headSentinel: DoublyNode Pointer
field tailSentinel: DoublyNode Pointer
field size: Integer <- 0
CONSTRUCTOR():
this.headSentinel <- new DoublyNode(DEFAULT)
this.tailSentinel <- new DoublyNode(DEFAULT)
this.headSentinel.next <- this.tailSentinel
this.tailSentinel.prev <- this.headSentinel
this.size <- 0
FUNCTION InsertAfterNode(targetNode: DoublyNode, val: ValueType) -> DoublyNode:
newNode <- new DoublyNode(val)
successor <- targetNode.next
newNode.prev <- targetNode
newNode.next <- successor
targetNode.next <- newNode
successor.prev <- newNode
this.size <- this.size + 1
return newNode
FUNCTION InsertAtHead(val: ValueType) -> DoublyNode:
return this.InsertAfterNode(this.headSentinel, val)
FUNCTION InsertAtTail(val: ValueType) -> DoublyNode:
return this.InsertAfterNode(this.tailSentinel.prev, val)
FUNCTION DeleteNode(targetNode: DoublyNode) -> ValueType:
if targetNode == this.headSentinel or targetNode == this.tailSentinel:
raise BoundaryException("Cannot delete sentinel nodes")
predecessor <- targetNode.prev
successor <- targetNode.next
predecessor.next <- successor
successor.prev <- predecessor
val <- targetNode.data
free(targetNode)
this.size <- this.size - 1
return val
FUNCTION DeleteHead() -> ValueType:
if this.size == 0:
raise UnderflowException("List is empty")
return this.DeleteNode(this.headSentinel.next)
FUNCTION DeleteTail() -> ValueType:
if this.size == 0:
raise UnderflowException("List is empty")
return this.DeleteNode(this.tailSentinel.prev)Topic 26: Circular Linked Lists (CSLL & CDLL)
#1. Structural Variations & Topologies
#A Circular Linked List eliminates terminal NULL pointers by connecting the final node back to the initial node, forming an unbroken ring buffer.
Comparison of Circular Topologies
| Property | Circular Singly Linked List (CSLL) | Circular Doubly Linked List (CDLL) |
|---|---|---|
| Pointer Count per Node | 1 (next) | 2 (prev, next) |
| Loop-Back Condition | tail.next == head | head.prev == tail and tail.next == head |
| Traversal Direction | Unidirectional (forward only) | Bidirectional (forward and backward) |
| Memory Overhead | 8 bytes pointer / node | 16 bytes pointer / node |
| Primary Use Cases | Round-robin CPU schedulers | Media playlist loops, Fibonacci heaps |
Circular Singly Linked List:
HEAD -> [ 10 ] -> [ 20 ] -> [ 30 ] (TAIL)
^ |
|----------------------------|
Circular Doubly Linked List:
|---------------------------------------------------------|
v |
[ 10 (HEAD) ] <==========> [ 20 ] <==========> [ 30 (TAIL) ]
| ^
|---------------------------------------------------------|2. The Single-tail Pointer Architecture for CSLL
#In a standard singly linked list, maintaining a pointer to HEAD requires an traversal to append at the tail. In a Circular Singly Linked List, maintaining only a pointer to TAIL provides instant access to both ends:
- Tail Access: Direct via
tail. - Head Access: Direct via
tail.next! - Insert At Head: Insert a new node between
tailandtail.next. - Insert At Tail: Insert between
tailandtail.next, then advancetail = newNode.
FUNCTION InsertAtHeadCSLL(tail: Node Pointer, val: ValueType) -> Node Pointer:
newNode <- new Node(val)
if tail == NULL:
newNode.next <- newNode
return newNode
newNode.next <- tail.next
tail.next <- newNode
return tail
FUNCTION InsertAtTailCSLL(tail: Node Pointer, val: ValueType) -> Node Pointer:
newNode <- new Node(val)
if tail == NULL:
newNode.next <- newNode
return newNode
newNode.next <- tail.next
tail.next <- newNode
return newNode // newNode is the new tail3. Classic Application: The Josephus Problem
#Problem Formulation
people stand in a circle labeled through . Beginning at person , counting proceeds clockwise. Every -th person is eliminated. The circle closes and counting resumes from the person immediately following the eliminated individual. The goal is to determine the safe starting position that guarantees survival.
Analytical Formula for
When (every second person is eliminated), the problem admits an elegant closed-form solution based on powers of 2:
For :
Simulation via Circular Linked List ()
| Round | Active Circle Sequence | Elimination Step () | Eliminated Person | Remaining Circle |
|---|---|---|---|---|
| 1 | Count 1 (Person 1), Count 2 (Person 2) | Person 2 | ||
| 2 | Count 1 (Person 3), Count 2 (Person 4) | Person 4 | ||
| 3 | Count 1 (Person 5), Count 2 (Person 1) | Person 1 | ||
| 4 | Count 1 (Person 3), Count 2 (Person 5) | Person 5 | Person 3 (Survivor) |
The simulation confirms the theoretical derivation: Person 3 survives.
FUNCTION JosephusSurvivor(n: Integer, k: Integer) -> Integer:
head <- new Node(1)
prev <- head
for i from 2 to n:
curr <- new Node(i)
prev.next <- curr
prev <- curr
prev.next <- head // Close the circular ring
curr <- head
while curr.next != curr:
for step from 1 to k - 2:
curr <- curr.next
// Delete the k-th node
eliminated <- curr.next
curr.next <- eliminated.next
free(eliminated)
curr <- curr.next
survivor <- curr.data
free(curr)
return survivorSimulation Complexity: time, auxiliary space.
4. Key Takeaways
#- Bidirectional Navigation: Doubly linked lists trade bytes of pointer overhead per node for true deletion of arbitrary nodes and bidirectional traversal.
- Sentinel Pattern: Invariant dummy head and tail nodes eliminate null pointer dereferences and special boundary checks.
- Single Tail Optimization: Storing only a
tailreference in a Circular Singly Linked List grants operations at both head (tail.next) and tail (tail). - Natural Ring Topologies: Circular lists are the natural data structure for round-robin CPU schedulers, periodic ring buffers, and cyclic elimination simulations.
Academic Attribution & References
#- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.), Chapter 10: Elementary Data Structures. MIT Press.
- Knuth, D. E. (1997). The Art of Computer Programming, Volume 1: Fundamental Algorithms (3rd ed.), Section 2.2: Linear Lists. Addison-Wesley.
- Graham, R. L., Knuth, D. E., & Patashnik, O. (1994). Concrete Mathematics: A Foundation for Computer Science (2nd ed.), Section 1.3: The Josephus Problem. Addison-Wesley.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.), Section 1.3: Bags, Queues, and Stacks. Addison-Wesley.