DSA Notes
Master circular linked lists — singly and doubly circular variants, complete implementation with insertion and deletion, traversal without infinite loops, detecting circular structure, and real-world applications in round-robin scheduling and circular buffers.
Introduction
A circular linked list is a linked list where the last node's next pointer points back to the first node (the head), forming a continuous loop. Unlike a singly or doubly linked list, there is no None at the end.
This circular structure naturally models scenarios that loop indefinitely: round-robin CPU scheduling (cycle through processes), multiplayer game turns (cycle through players), carousel navigation (cycle through slides), and circular buffers (cycle through a fixed-size buffer).
Node Structure
class CircularNode:
def __init__(self, data):
self.data = data
self.next = None # For circular singly
# self.prev = None # For circular doubly
def __repr__(self):
return f"CNode({self.data})"Singly Circular Linked List — Complete Implementation
class CircularLinkedList:
"""
Singly circular linked list.
We keep a pointer to the TAIL (not head) for O(1) append and prepend.
head can be accessed as tail.next.
"""
def __init__(self):
self.tail = None # Points to last node
self._size = 0
@property
def head(self):
return self.tail.next if self.tail else None
def is_empty(self):
return self.tail is None
def __len__(self):
return self._sizeWhy Keep a Tail Pointer (Not Head)?
With a tail pointer:
- Access head:
tail.next→ O(1) - Insert at head: Create node, set
node.next = tail.next,tail.next = node→ O(1) - Insert at tail: Create node, append after tail, update tail → O(1)
With only a head pointer, inserting at tail would require O(n) traversal to find the last node.
Insertion Operations
def insert_at_head(self, data):
"""Insert at the beginning. O(1)."""
new_node = CircularNode(data)
if self.tail is None:
# Single node points to itself
new_node.next = new_node
self.tail = new_node
else:
new_node.next = self.tail.next # new → old head
self.tail.next = new_node # tail → new head
self._size += 1
def insert_at_tail(self, data):
"""Insert at the end. O(1)."""
new_node = CircularNode(data)
if self.tail is None:
new_node.next = new_node
self.tail = new_node
else:
new_node.next = self.tail.next # new → head
self.tail.next = new_node # old tail → new
self.tail = new_node # Update tail
self._size += 1Visualization — insert 4 at tail when list is [1,2,3]:
Before
tail=3, 3.next=1
1 → 2 → 3 → (back to 1)
Insert 4 at tail
4.next = tail.next = 1 → (4 points to head)
tail.next = 4 → (3 points to 4)
tail = 4 → (4 is new tail)
After
tail=4, 4.next=1
1 → 2 → 3 → 4 → (back to 1)
Traversal — Must Detect Full Circle
def traverse(self):
"""
Traverse all nodes exactly once.
Stop when we return to the head node.
"""
if self.tail is None:
print("Empty list")
return
current = self.tail.next # Start at head
result = []
while True:
result.append(str(current.data))
current = current.next
if current == self.tail.next: # Back at head — full circle done
break
print(" → ".join(result) + " → (back to head)")
def contains(self, value):
"""Search for a value. O(n)."""
if self.tail is None:
return False
current = self.tail.next # Start at head
while True:
if current.data == value:
return True
current = current.next
if current == self.tail.next: # Completed full circle
break
return FalseDeletion Operations
def delete_head(self):
"""Remove the first element. O(1)."""
if self.tail is None:
raise ValueError("Empty list")
if self.tail.next == self.tail: # Only one node
removed = self.tail.data
self.tail = None
self._size -= 1
return removed
head = self.tail.next
removed = head.data
self.tail.next = head.next # Tail points to new head
self._size -= 1
return removed
def delete_tail(self):
"""Remove the last element. O(n) — must find second-to-last."""
if self.tail is None:
raise ValueError("Empty list")
if self.tail.next == self.tail: # Only one node
removed = self.tail.data
self.tail = None
self._size -= 1
return removed
# Find the node just before tail
current = self.tail.next # Start at head
while current.next != self.tail:
current = current.next
removed = self.tail.data
current.next = self.tail.next # Second-to-last → head
self.tail = current # Update tail
self._size -= 1
return removed
def delete_value(self, value):
"""Delete first node with given value. O(n)."""
if self.tail is None:
return False
head = self.tail.next
# Check if head is the target
if head.data == value:
self.delete_head()
return True
# Check if tail is the target
if self.tail.data == value:
self.delete_tail()
return True
# Traverse to find the node before the target
current = head
while current.next != head: # Until we loop back to head
if current.next.data == value:
current.next = current.next.next
if current.next == head: # Deleted node was tail? No — tail is handled above
pass
self._size -= 1
return True
current = current.next
return FalseDoubly Circular Linked List
For scenarios requiring both directions:
class DoublyCircularNode:
def __init__(self, data):
self.data = data
self.next = None
self.prev = None
class DoublyCircularList:
def __init__(self):
# Use sentinel node (dummy) for cleaner implementation
# Sentinel.next = first real node, Sentinel.prev = last real node
self.sentinel = DoublyCircularNode(None)
self.sentinel.next = self.sentinel
self.sentinel.prev = self.sentinel
self._size = 0
def insert_at_head(self, data):
new_node = DoublyCircularNode(data)
head = self.sentinel.next
new_node.next = head
new_node.prev = self.sentinel
self.sentinel.next = new_node
head.prev = new_node
self._size += 1
def insert_at_tail(self, data):
new_node = DoublyCircularNode(data)
tail = self.sentinel.prev
new_node.prev = tail
new_node.next = self.sentinel
tail.next = new_node
self.sentinel.prev = new_node
self._size += 1
def delete_node(self, node):
"""Delete any node in O(1)."""
node.prev.next = node.next
node.next.prev = node.prev
self._size -= 1
def traverse(self):
current = self.sentinel.next
result = []
while current != self.sentinel:
result.append(str(current.data))
current = current.next
print(" ↔ ".join(result) + " ↔ (circular)")Detecting a Circular List
If you receive an unknown linked list and need to check if it is circular:
def is_circular(head):
"""
Detect if a linked list is circular (has a cycle that includes head).
Uses Floyd's algorithm.
Time: O(n), Space: O(1)
"""
if head is None:
return False
slow = head
fast = head.next if head.next else None
while fast is not None and fast != slow:
slow = slow.next
fast = fast.next
if fast:
fast = fast.next
# If fast == slow, there is a cycle
# Additionally check if it cycles back to head (truly circular, not just a loop)
if fast == slow == head:
return True
# More general: check if last node points to head
current = head
while current.next and current.next != head:
current = current.next
return current.next == headReal-World Applications
1. Round-Robin CPU Scheduling
class RoundRobinScheduler:
"""
Simulate round-robin CPU scheduling using circular linked list.
Each process gets equal time slices, cycling through all processes.
"""
def __init__(self, time_quantum=2):
self.cll = CircularLinkedList()
self.time_quantum = time_quantum
def add_process(self, process_id, burst_time):
# Store (process_id, remaining_time) as data
self.cll.insert_at_tail((process_id, burst_time))
print(f"Added Process P{process_id} (burst={burst_time})")
def run(self):
"""Execute all processes with round-robin scheduling."""
if self.cll.is_empty():
return
current = self.cll.tail.next # Start at head
time = 0
while not self.cll.is_empty():
pid, remaining = current.data
# Execute for time_quantum or remaining time
execute_time = min(self.time_quantum, remaining)
remaining -= execute_time
time += execute_time
print(f"Time {time}: P{pid} executed for {execute_time} units"
+ (" → Done" if remaining == 0 else f" → {remaining} remaining"))
next_node = current.next
if remaining == 0:
self.cll.delete_value(current.data) # Process complete
else:
current.data = (pid, remaining) # Update remaining time
current = next_node if not self.cll.is_empty() else None
if current and current == self.cll.head and self.cll.is_empty():
break2. Circular Buffer (Ring Buffer)
Summary
| Feature | Singly Circular | Doubly Circular |
|---|---|---|
| Traversal direction | Forward only | Both directions |
| Memory per node | data + 1 ptr | data + 2 ptrs |
| Delete given node | O(n) | O(1) |
| Head access (tail ptr) | O(1) | O(1) |
| Tail access (tail ptr) | O(1) | O(1) |
| End of list indicator | node.next == head | node.next == sentinel |
Key difference from linear linked lists: There is no None at the end. Traversal must use a "have we looped back?" condition to avoid infinite loops.
Real-world uses:
- Round-robin scheduling
- Circular buffer / ring buffer
- Multiplayer game turn management
- Carousel/slideshow UI
- Fibonacci heap (advanced data structure)
*Next Lesson: Linked List Operations — Deep dive into every linked list operation pattern used in competitive programming and interviews.*
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Circular Linked List — Structure, Operations, and Applications.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Data Structures & Algorithms topic.
Search Terms
data-structures-algorithms, data structures & algorithms, data, structures, algorithms, linked, lists, circular
Related Data Structures & Algorithms Topics