Circular Linked List
A circular linked list is a variation of a linked list where the last node points back to the first node, forming a circle. This allows for continuous traversal without encountering null references and enables unique operations like rotation.
Features
- Continuous traversal - no null references, can traverse infinitely
- Dynamic size - grows and shrinks during runtime
- Fast insertion/deletion at both ends (O(1))
- Rotation support - efficient rotation operations
- Memory efficient - allocates memory as needed
- Generic type support for type safety
Time Complexities
| Operation | Time Complexity |
|---|---|
| Prepend | O(1) |
| Append | O(1) |
| Insert | O(n) |
| Remove First | O(1) |
| Remove Last | O(n) |
| Remove by Value | O(n) |
| Remove by Index | O(n) |
| Search | O(n) |
| Access by Index | O(n) |
| Rotate | O(k) where k is number of steps |
Basic Usage
Creating and Initializing
from algo.data_structs.lists.circular_linked_list import CircularLinkedList
# Create empty list
cll = CircularLinkedList()
# Check if empty
print(cll.is_empty()) # True
print(len(cll)) # 0
print(cll.size()) # 0
Adding Elements
# Prepend elements (add to beginning)
cll.prepend(3)
cll.prepend(2)
cll.prepend(1)
print(list(cll)) # [1, 2, 3]
# Append elements (add to end)
cll.append(4)
cll.append(5)
print(list(cll)) # [1, 2, 3, 4, 5]
# Extend with multiple elements
cll.extend([6, 7, 8])
print(list(cll)) # [1, 2, 3, 4, 5, 6, 7, 8]
# Insert at specific position
cll.insert(0, 0) # Insert at beginning
cll.insert(4, 3.5) # Insert in middle
cll.insert(len(cll), 9) # Insert at end
print(list(cll)) # [0, 1, 2, 3, 3.5, 4, 5, 6, 7, 8, 9]
Accessing Elements
# Get head and tail
print(cll.get_head()) # 0 (first element)
print(cll.get_tail()) # 9 (last element)
# Access by index
print(cll.get(0)) # 0 (first element)
print(cll.get(5)) # 4 (sixth element)
# Modify elements
cll.set(1, 10)
print(cll.get(1)) # 10
# Get list length
print(len(cll)) # 11
print(cll.size()) # 11
Removing Elements
# Remove first element
first = cll.remove_first()
print(first) # 0
print(list(cll)) # [10, 2, 3, 3.5, 4, 5, 6, 7, 8, 9]
# Remove last element
last = cll.remove_last()
print(last) # 9
print(list(cll)) # [10, 2, 3, 3.5, 4, 5, 6, 7, 8]
# Remove by value (first occurrence)
result = cll.remove(3.5)
print(result) # True
print(list(cll)) # [10, 2, 3, 4, 5, 6, 7, 8]
# Remove by index
removed = cll.remove_at(0)
print(removed) # 10
print(list(cll)) # [2, 3, 4, 5, 6, 7, 8]
# Clear all elements
cll.clear()
print(len(cll)) # 0
Circular Nature and Continuous Traversal
The key feature of circular linked lists is their circular structure:
cll = CircularLinkedList()
cll.extend([1, 2, 3])
# The list forms a circle: 1 -> 2 -> 3 -> 1 -> 2 -> 3 -> ...
print(str(cll)) # CircularLinkedList([1 -> 2 -> 3 -> (head)])
# Standard iteration respects the actual size
for item in cll:
print(item) # 1, 2, 3 (stops after one complete cycle)
# Manual traversal can continue indefinitely
# Note: This is just conceptual - don't run infinite loops!
def traverse_n_times(cll, n_cycles):
"""Traverse the circular list for n complete cycles."""
if cll.is_empty():
return []
result = []
current_node = cll.head
total_elements = len(cll) * n_cycles
for _ in range(total_elements):
result.append(current_node.data)
current_node = current_node.next
return result
# Traverse 2 complete cycles
two_cycles = traverse_n_times(cll, 2)
print(two_cycles) # [1, 2, 3, 1, 2, 3]
Rotation Operations
Circular linked lists excel at rotation operations:
cll = CircularLinkedList()
cll.extend([1, 2, 3, 4, 5])
# Rotate right (positive steps)
cll.rotate(1) # Move head one position to the right
print(list(cll)) # [5, 1, 2, 3, 4]
cll.rotate(2) # Rotate right by 2 more positions
print(list(cll)) # [3, 4, 5, 1, 2]
# Rotate left (negative steps)
cll.rotate(-1) # Move head one position to the left
print(list(cll)) # [4, 5, 1, 2, 3]
# Rotation normalizes steps automatically
cll.rotate(7) # 7 % 5 = 2 effective steps
print(list(cll)) # [2, 3, 4, 5, 1]
# Reset for next examples
cll.clear()
cll.extend([1, 2, 3, 4, 5])
Search Operations
cll = CircularLinkedList()
cll.extend([1, 2, 3, 2, 4, 2])
# Find index of element (first occurrence)
print(cll.find(2)) # 1
print(cll.find(4)) # 4
print(cll.find(99)) # -1 (not found)
# Check if element exists
print(2 in cll) # True
print(99 in cll) # False
# Count occurrences
print(cll.count(2)) # 3
print(cll.count(1)) # 1
print(cll.count(99)) # 0 (non-existent)
List Manipulation
cll = CircularLinkedList()
cll.extend([1, 2, 3, 4, 5])
# Reverse the list
cll.reverse()
print(list(cll)) # [5, 4, 3, 2, 1]
# Copy the list
cll_copy = cll.copy()
print(list(cll_copy)) # [5, 4, 3, 2, 1]
# Lists are independent
cll_copy.append(6)
print(len(cll)) # 5 (original unchanged)
print(len(cll_copy)) # 6 (copy modified)
# Convert to Python list
python_list = cll.to_list()
print(python_list) # [5, 4, 3, 2, 1]
print(type(python_list)) # <class 'list'>
Advanced Operations
Splitting Lists
cll = CircularLinkedList()
cll.extend([1, 2, 3, 4, 5, 6])
# Split at index 3
second_part = cll.split_at(3)
print(list(cll)) # [1, 2, 3] (first part)
print(list(second_part)) # [4, 5, 6] (second part)
# Both parts remain circular
print(str(cll)) # CircularLinkedList([1 -> 2 -> 3 -> (head)])
print(str(second_part)) # CircularLinkedList([4 -> 5 -> 6 -> (head)])
# Split at beginning (entire list becomes second part)
cll.extend([7, 8])
all_elements = cll.split_at(0)
print(list(cll)) # [] (empty)
print(list(all_elements)) # [1, 2, 3, 7, 8]
# Split at end (second part is empty)
cll.extend([1, 2, 3])
empty_part = cll.split_at(len(cll))
print(list(cll)) # [1, 2, 3]
print(list(empty_part)) # []
List Comparison
cll1 = CircularLinkedList()
cll1.extend([1, 2, 3])
cll2 = CircularLinkedList()
cll2.extend([1, 2, 3])
cll3 = CircularLinkedList()
cll3.extend([1, 2, 4])
print(cll1 == cll2) # True (same elements)
print(cll1 == cll3) # False (different elements)
# Note: Rotation affects equality
cll2.rotate(1)
print(cll1 == cll2) # False ([1,2,3] != [3,1,2])
Working with Different Types
Strings
str_cll = CircularLinkedList()
str_cll.extend(['apple', 'banana', 'cherry'])
print(str_cll.find('banana')) # 1
print(str_cll.count('apple')) # 1
print('cherry' in str_cll) # True
# Rotation with strings
str_cll.rotate(1)
print(list(str_cll)) # ['cherry', 'apple', 'banana']
Mixed Types
mixed_cll = CircularLinkedList()
mixed_cll.extend([1, 'hello', 3.14, True])
print(len(mixed_cll)) # 4
print(mixed_cll.get(1)) # 'hello'
print(mixed_cll.get(3)) # True
Custom Objects
class Task:
def __init__(self, name: str, priority: int):
self.name = name
self.priority = priority
def __eq__(self, other):
return isinstance(other, Task) and self.name == other.name
def __str__(self):
return f"Task({self.name}, priority={self.priority})"
tasks = CircularLinkedList()
task1 = Task("Email", 1)
task2 = Task("Meeting", 2)
task3 = Task("Code Review", 3)
tasks.extend([task1, task2, task3])
print(tasks.count(task1)) # 1
print(tasks.find(task2)) # 1
# Rotate tasks (like a round-robin scheduler)
tasks.rotate(1)
for task in tasks:
print(task) # Task(Code Review, priority=3), Task(Email, priority=1), Task(Meeting, priority=2)
Performance Characteristics
Insertion/Deletion Performance
- Prepend/Append: O(1) - Direct access to head/tail
- Insert/Remove at arbitrary position: O(n) - Must traverse to position
- Remove first: O(1) - Direct head access
- Remove last: O(n) - Must find second-to-last node
cll = CircularLinkedList()
# Fast operations at ends
for i in range(1000):
cll.append(i) # O(1) each
cll.prepend(-i) # O(1) each
# Fast removal from beginning
while len(cll) > 0:
cll.remove_first() # O(1) each
Rotation Performance
Rotation is very efficient compared to other list types:
cll = CircularLinkedList()
cll.extend(range(1000))
# Efficient rotation - only changes head/tail pointers
cll.rotate(100) # O(100) - much faster than rebuilding list
cll.rotate(-50) # O(50)
# Compare with equivalent operations on regular list
regular_list = list(range(1000))
# To rotate right by 100: regular_list = regular_list[-100:] + regular_list[:-100] # O(n)
Error Handling
cll = CircularLinkedList()
# Index errors for empty list
try:
cll.get(0) # IndexError: Index 0 out of range for list of size 0
except IndexError:
print("Cannot access empty list")
try:
cll.remove_first() # IndexError: Cannot remove from empty list
except IndexError:
print("Cannot remove from empty list")
# Index errors for invalid indices
cll.extend([1, 2, 3])
try:
cll.get(5) # IndexError: Index 5 out of range for list of size 3
except IndexError:
print("Index out of range")
try:
cll.insert(-1, 5) # IndexError: Insert index -1 out of range [0, 3]
except IndexError:
print("Invalid insert index")
try:
cll.split_at(5) # IndexError: Split index 5 out of range [0, 3]
except IndexError:
print("Invalid split index")
String Representation
cll = CircularLinkedList()
cll.extend([1, 2, 3])
print(str(cll)) # CircularLinkedList([1 -> 2 -> 3 -> (head)])
print(repr(cll)) # CircularLinkedList(size=3, head=CircularListNode(1), tail=CircularListNode(3))
# Empty list representation
empty_cll = CircularLinkedList()
print(str(empty_cll)) # CircularLinkedList([])
Common Patterns
Round-Robin Scheduling
def round_robin_scheduler(tasks, time_slice):
"""Simulate round-robin task scheduling."""
if not tasks:
return []
schedule = CircularLinkedList()
schedule.extend(tasks)
execution_order = []
current_time = 0
while not schedule.is_empty():
# Get current task
current_task = schedule.get_head()
execution_order.append((current_task, current_time))
# Remove completed task or rotate to next
if current_task.remaining_time <= time_slice:
current_time += current_task.remaining_time
schedule.remove_first()
else:
current_task.remaining_time -= time_slice
current_time += time_slice
schedule.rotate(1) # Move to next task
return execution_order
# Example usage
class SchedulerTask:
def __init__(self, name, remaining_time):
self.name = name
self.remaining_time = remaining_time
def __str__(self):
return f"{self.name}({self.remaining_time})"
tasks = [
SchedulerTask("Task1", 10),
SchedulerTask("Task2", 5),
SchedulerTask("Task3", 8)
]
schedule = round_robin_scheduler(tasks, 3)
for task, time in schedule:
print(f"Execute {task} at time {time}")
Circular Buffer Implementation
class CircularBuffer:
"""Fixed-size circular buffer using circular linked list."""
def __init__(self, capacity):
self.capacity = capacity
self.buffer = CircularLinkedList()
def put(self, item):
if len(self.buffer) < self.capacity:
self.buffer.append(item)
else:
# Overwrite oldest item
self.buffer.remove_first()
self.buffer.append(item)
def get(self):
if self.buffer.is_empty():
raise IndexError("Buffer is empty")
return self.buffer.remove_first()
def peek(self):
return self.buffer.get_head()
def is_full(self):
return len(self.buffer) == self.capacity
def __str__(self):
return f"CircularBuffer({list(self.buffer)})"
# Example usage
buffer = CircularBuffer(3)
buffer.put("A")
buffer.put("B")
buffer.put("C")
print(buffer) # CircularBuffer(['A', 'B', 'C'])
buffer.put("D") # Overwrites "A"
print(buffer) # CircularBuffer(['B', 'C', 'D'])
Playlist with Shuffle and Repeat
class MusicPlaylist:
"""Music playlist supporting shuffle and repeat modes."""
def __init__(self):
self.songs = CircularLinkedList()
self.current_position = 0
def add_song(self, song):
self.songs.append(song)
def next_song(self):
if self.songs.is_empty():
return None
song = self.songs.get(self.current_position)
self.current_position = (self.current_position + 1) % len(self.songs)
return song
def previous_song(self):
if self.songs.is_empty():
return None
self.current_position = (self.current_position - 1) % len(self.songs)
return self.songs.get(self.current_position)
def shuffle(self):
"""Rotate the playlist by a random amount."""
import random
if len(self.songs) > 1:
steps = random.randint(1, len(self.songs) - 1)
self.songs.rotate(steps)
self.current_position = 0
# Example usage
playlist = MusicPlaylist()
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
print(playlist.next_song()) # Song A
print(playlist.next_song()) # Song B
print(playlist.next_song()) # Song C
print(playlist.next_song()) # Song A (circular!)
playlist.shuffle()
print("After shuffle:", playlist.next_song())
Use Cases
When to Use Circular Linked Lists
Good for:
- Round-robin scheduling algorithms
- Circular buffers and ring buffers
- Music playlists with repeat functionality
- Game turns (multiplayer games)
- Resource allocation in round-robin fashion
- Implementation of circular queues
Examples:
# CPU process scheduling
cpu_scheduler = CircularLinkedList()
cpu_scheduler.extend(["Process1", "Process2", "Process3"])
# Operating system resource sharing
shared_resources = CircularLinkedList()
shared_resources.extend(["Printer1", "Printer2", "Scanner"])
# Game turn management
players = CircularLinkedList()
players.extend(["Alice", "Bob", "Charlie", "Diana"])
# Network load balancing
servers = CircularLinkedList()
servers.extend(["Server1", "Server2", "Server3"])
def next_server():
"""Get next server in round-robin fashion."""
current = servers.get_head()
servers.rotate(1)
return current
When NOT to Use Circular Linked Lists
Avoid for:
- When you need frequent random access by index
- Simple sequential processing without circular behavior
- Memory-constrained environments (overhead of maintaining circularity)
- When standard linear data access patterns are sufficient
Better alternatives:
# For random access, use dynamic array
from algo.data_structs.arrays.dynamic_array import DynamicArray
arr = DynamicArray() # Better for arr[index] operations
# For simple sequential processing, use regular linked list
from algo.data_structs.lists.singly_linked_list import SinglyLinkedList
sll = SinglyLinkedList() # Simpler when circularity not needed
# For queue operations without rotation, use deque
from collections import deque
queue = deque() # More efficient for pure queue operations
Node Structure
The underlying CircularListNode class provides the building blocks:
from algo.data_structs.lists.circular_linked_list import CircularListNode
# Create individual nodes
node1 = CircularListNode(1)
node2 = CircularListNode(2)
node3 = CircularListNode(3)
# Link nodes manually to form a circle
node1.next = node2
node2.next = node3
node3.next = node1 # Complete the circle
print(node1.data) # 1
print(node1.next) # node2
print(node2.next) # node3
print(node3.next) # node1 (back to start)
# String representation
print(str(node1)) # "1"
print(repr(node1)) # "CircularListNode(1)"
Performance Comparison
| Operation | Circular Linked List | Singly Linked List | Doubly Linked List | Dynamic Array |
|---|---|---|---|---|
| Prepend | O(1) | O(1) | O(1) | O(n) |
| Append | O(1) | O(n) | O(1) | O(1) amortized |
| Insert Middle | O(n) | O(n) | O(n) | O(n) |
| Remove First | O(1) | O(1) | O(1) | O(n) |
| Remove Last | O(n) | O(n) | O(1) | O(1) |
| Access by Index | O(n) | O(n) | O(n) | O(1) |
| Rotation | O(k) | N/A | N/A | O(n) |
| Circular Traversal | Native | Manual | Manual | Manual |
| Memory Overhead | Low | Low | Medium | Medium |
Advanced Features
Custom Node Access
cll = CircularLinkedList()
cll.extend([1, 2, 3, 4, 5])
# Access internal nodes for advanced operations
# Note: This accesses private implementation details
current_node = cll.head
for i in range(10): # Traverse multiple cycles
print(f"Position {i}: {current_node.data}")
current_node = current_node.next
# Output: 1, 2, 3, 4, 5, 1, 2, 3, 4, 5
Memory Efficiency
# Circular linked lists are memory efficient for their use cases
cll = CircularLinkedList()
# Each node only stores data + one pointer (like singly linked list)
# Plus two additional pointers (head + tail) for the entire list
# No additional overhead for maintaining circularity
for i in range(1000):
cll.append(i) # Each append allocates one node
# Rotation doesn't require additional memory
cll.rotate(500) # O(500) time, O(1) additional space
API Reference
Circular Linked List Implementation
A circular linked list is a variation of a linked list where the last node points back to the first node, forming a circle. This allows for continuous traversal without encountering null references.
Key Features:
- Dynamic size allocation
- Continuous traversal capability
- No null references (except for empty list)
- Efficient insertion/deletion at any position with a reference
Time Complexities:
- Access: O(n) - must traverse from current position
- Search: O(n) - linear search required
- Insertion: O(1) with node reference, O(n) at arbitrary position
- Deletion: O(1) with node reference, O(n) at arbitrary position
- Append/Prepend: O(1) when maintaining tail reference
Space Complexity: O(n)
CircularLinkedList()
Bases: Generic[T]
A circular linked list implementation with comprehensive operations.
This implementation maintains references to both head and tail nodes for efficient operations at both ends, and tracks the size for efficient length operations.
Initialize an empty circular linked list.
__contains__(value)
Check if a value exists in the list.
__eq__(other)
Check if two circular linked lists are equal.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Another CircularLinkedList to compare with |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if lists contain the same elements in the same order |
__iter__()
Return an iterator over the list elements.
__len__()
Return the number of elements in the list.
__repr__()
Return detailed string representation for debugging.
__str__()
Return string representation of the list.
append(data)
Add an element to the end of the list.
Time Complexity: O(1)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Element to add |
required |
clear()
Remove all elements from the list.
Time Complexity: O(1)
copy()
Create a shallow copy of the list.
Time Complexity: O(n)
Returns:
| Type | Description |
|---|---|
CircularLinkedList[T]
|
A new CircularLinkedList with the same elements |
count(data)
Count the number of occurrences of the specified element.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Element to count |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of occurrences |
extend(iterable)
Extend the list by appending all elements from the iterable.
Time Complexity: O(k) where k is the number of elements to add
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
iterable
|
Iterable containing elements to add |
required |
find(data)
Find the index of the first occurrence of the specified element.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Element to find |
required |
Returns:
| Type | Description |
|---|---|
int
|
Index of the first occurrence, or -1 if not found |
get(index)
Get element at the specified index.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Index of element to retrieve |
required |
Returns:
| Type | Description |
|---|---|
T
|
Element at the specified index |
Raises:
| Type | Description |
|---|---|
IndexError
|
If index is out of bounds |
get_head()
Get the first element without removing it.
Time Complexity: O(1)
Returns:
| Type | Description |
|---|---|
Optional[T]
|
The first element, or None if list is empty |
get_tail()
Get the last element without removing it.
Time Complexity: O(1)
Returns:
| Type | Description |
|---|---|
Optional[T]
|
The last element, or None if list is empty |
insert(index, data)
Insert an element at the specified index.
Time Complexity: O(n) - may need to traverse to the position
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Index where to insert (0-based) |
required |
data
|
T
|
Element to insert |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If index is out of valid range |
is_empty()
Check if the list is empty.
Returns:
| Type | Description |
|---|---|
bool
|
True if the list is empty, False otherwise |
prepend(data)
Add an element to the beginning of the list.
Time Complexity: O(1)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Element to add |
required |
remove(data)
Remove the first occurrence of the specified element.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
Element to remove |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if element was found and removed, False otherwise |
remove_at(index)
Remove and return element at the specified index.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Index of element to remove |
required |
Returns:
| Type | Description |
|---|---|
T
|
The removed element |
Raises:
| Type | Description |
|---|---|
IndexError
|
If index is out of bounds |
remove_first()
Remove and return the first element.
Time Complexity: O(1)
Returns:
| Type | Description |
|---|---|
T
|
The removed element |
Raises:
| Type | Description |
|---|---|
IndexError
|
If the list is empty |
remove_last()
Remove and return the last element.
Time Complexity: O(n) - must traverse to find the second-to-last node
Returns:
| Type | Description |
|---|---|
T
|
The removed element |
Raises:
| Type | Description |
|---|---|
IndexError
|
If the list is empty |
reverse()
Reverse the list in place.
Time Complexity: O(n) Space Complexity: O(1)
rotate(steps=1)
Rotate the list by the specified number of steps.
Positive steps rotate to the right (tail becomes new head). Negative steps rotate to the left (head moves forward).
Time Complexity: O(k) where k is the effective number of steps
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
steps
|
int
|
Number of steps to rotate (default: 1) |
1
|
set(index, data)
Set element at the specified index.
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Index where to set the value |
required |
data
|
T
|
New value to set |
required |
Raises:
| Type | Description |
|---|---|
IndexError
|
If index is out of bounds |
size()
Get the number of elements in the list.
Returns:
| Type | Description |
|---|---|
int
|
The number of elements |
split_at(index)
Split the list at the specified index, returning the second part.
The original list will contain elements [0, index) and the returned list will contain elements [index, size).
Time Complexity: O(n)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
Index where to split (0 <= index <= size) |
required |
Returns:
| Type | Description |
|---|---|
CircularLinkedList[T]
|
A new CircularLinkedList containing the second part |
Raises:
| Type | Description |
|---|---|
IndexError
|
If index is out of valid range |
to_list()
Convert the circular linked list to a Python list.
Time Complexity: O(n)
Returns:
| Type | Description |
|---|---|
list[T]
|
A Python list containing all elements |
CircularListNode(data, next_node=None)
Bases: Generic[T]
A node in a circular linked list.
Each node contains data and a reference to the next node.
Initialize a new circular list node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
T
|
The data to store in this node |
required |
next_node
|
Optional[CircularListNode[T]]
|
Reference to the next node (default: None) |
None
|
__repr__()
Return detailed string representation for debugging.
__str__()
Return string representation of the node.