Skip to content

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.