A circular linked list is a fundamental data structure used extensively in computer science. Unlike a regular singly linked list that has a null reference at the end, a circular linked list connects the last node back to the first node to form a continuous loop. This structure lends itself well to problems that involve traversal or rotation through elements.
In this comprehensive guide, we will explore how to implement a circular linked list in Python. We will cover the key aspects of building this data structure from scratch including:
- Defining a Node class
- Constructing a CircularLinkedList class
- Writing methods for insertion, deletion and traversal
- Testing the implementation with example usage
Proper understanding of pointers and references in Python is required to implement linked lists efficiently. So let’s get started!
Table of Contents
Open Table of Contents
Prerequisites
To follow along with the code examples, you should have:
- Basic knowledge of Python (variables, loops, functions, classes)
- Understanding of references vs values in Python
- Exposure to basic data structures like arrays or linked lists
- Familiarity with object-oriented programming concepts
We will be using Python 3.x in the code snippets. Feel free to code along in an IDE like PyCharm or Jupyter Notebook.
Node Class
The building block of a linked list is a node. Let’s define a Node
class in Python to represent each element in the list:
class Node:
def __init__(self, data):
self.data = data
self.next = None
A node contains two fields:
data
- to store the element valuenext
- to store the reference to the next node in the list
We initialize next
to None
indicating the lack of connection upon creation.
Circular Linked List Class
Now let’s utilize the Node
class to build our circular linked list data structure:
class CircularLinkedList:
def __init__(self):
self.head = None
The CircularLinkedList
class contains a single head
field pointing to the first node of the list. We initialize it to None
indicating an empty list.
Next, we will write methods to insert, delete and print elements in the circular linked list.
Inserting Elements
There are two ways to insert elements in a circular linked list:
- Insert at Head
- Insert at Tail
Let’s define both methods:
Insert at Head
This method inserts the given node at the beginning of the list.
def insertHead(self, data):
newNode = Node(data)
if self.head is None:
self.head = newNode
newNode.next = self.head
else:
current = self.head
while current.next != self.head:
current = current.next
current.next = newNode
newNode.next = self.head
self.head = newNode
The steps are:
- Create a new
Node
with the given data - Check if list is empty, if so directly assign it as head
- Otherwise, traverse to end of list and insert new node before head
- Update head to new node
Insert at Tail
This method inserts a new node at the end of the circular linked list:
def insertTail(self, data):
newNode = Node(data)
if self.head is None:
self.head = newNode
newNode.next = self.head
else:
current = self.head
while current.next != self.head:
current = current.next
current.next = newNode
newNode.next = self.head
The logic is very similar to insert at head:
- Create new node
- Check for empty list then directly insert
- Traverse to end, insert new node, and connect to head
With these two methods, we can insert elements at the front or back of the circular linked list.
Deleting Elements
For removing elements from a circular linked list, there are also two approaches:
- Delete from Head
- Delete from Tail
Let’s implement them:
Delete from Head
This method removes the first node of the list:
def deleteHead(self):
if self.head is None:
return None
current = self.head
while current.next != self.head:
current = current.next
toDelete = self.head
current.next = self.head.next
self.head = self.head.next
return toDelete.data
The logic is:
- Check for empty list
- Traverse to end and save last node
- Point last node to second node, removing head
- Update head and return old head’s data
Delete from Tail
This method removes the last node:
def deleteTail(self):
if self.head is None:
return None
current = self.head
while current.next.next != self.head:
current = current.next
toDelete = current.next
current.next = self.head
return toDelete.data
The steps are:
- Check for empty list
- Traverse to second last node
- Point second last node to head, removing last node
- Return data of removed node
Together, these two methods allow removing elements from either end of the circular linked list.
Traversing the List
Since a circular linked list loops back, we need to be careful when traversing it. We cannot stop when current.next
becomes None
as in a normal linked list.
Let’s write a printList
method to print the circular linked list:
def printList(self):
if self.head is None:
return
current = self.head
print(current.data, end=" ")
while current.next != self.head:
current = current.next
print(current.data, end=" ")
print()
It loops until the next
pointer again points back to the head, indicating the end of traversal.
We can also create a generator to yield the node values during traversal:
def traverse(self):
current = self.head
while current:
yield current.data
current = current.next
if current == self.head:
break
This allows iterating over the circular linked list using a for loop.
Putting It All Together
Let’s test our circular linked list implementation with a simple example:
cll = CircularLinkedList()
cll.insertHead(5)
cll.insertHead(10)
cll.insertTail(7)
print([node for node in cll.traverse()]) # [10, 5, 7]
cll.deleteHead()
cll.deleteTail()
print([node for node in cll.traverse()]) # [5]
This shows basic usage of the insert, delete and traverse methods we implemented. The circular list retains its structure and looped references even after deletions.
Some key points:
- The list cycles back on itself in a loop
- Insertions can happen at head or tail
- Deletions remove from head or tail
- Traversal requires looping until back to head
The circular linked list enables efficient insertions and deletions from both ends while maintaining constant access to the first node. This makes it effective for problems involving circular buffers or rotations.
Time and Space Complexity
Let’s analyze the time and space complexity of operations on our circular linked list implementation:
Time Complexity
- Access - O(n) - Need to traverse entire list to reach a node
- Search - O(n) - Search by traversing and comparing each element
- Insert - O(1) - Insert at head or tail by updating a few pointers
- Delete - O(1) - Delete from head or tail by updating a few pointers
Space Complexity
- Auxiliary Space - O(n) - Additional memory for the linked list nodes
Circular linked lists provide constant time insertions and deletions giving them an edge over arrays in certain use cases. The compromise is slow access and search due to linear traversal.
Applications
Some useful applications of circular linked lists include:
- Implementing circular buffers for queue operations
- Modelling periodic/cyclic conditions and patterns
- Representing concentric layers or relationships
- Maintaining computer processes queued for execution
- Implementing the game Snake or wheels of fortune/spinning cursors
- Linking audio samples for seamless looping playback
Game development, operating system processes scheduling, multimedia editing software are some domains that can benefit from circular linked lists.
Example Applications
Circular linked lists are commonly used in game development for seamless looping of sprites or motion. For example, a spaceship orbiting a planet can be modelled as nodes in a circular list with connections to enable smooth traversal along the orbit path. As new nodes are added or removed, the ship progresses along the orbit.
Operating systems also leverage circular linked lists to maintain processes queued for execution on the CPU. The OS scheduler loops through the circular list, running each process in turn for its allocated time slice. New processes are added to the list and existing ones removed as they terminate. The circular structure allows smooth, indefinite cycling through the queued processes.
In audio editing software, circular linked lists help enable seamless playback looping. Audio samples can be stored as nodes and connected in a circle. Playback simply traverses this circular list repeatedly, providing continuous uninterrupted playback. Inserting or deleting samples is also efficient, making editing seamless.
Hope these real-world examples help illustrate the power and flexibility of circular linked lists for problems involving cyclic continuity! Let me know if you have any other questions.
Conclusion
In this guide, we learned how to implement a circular linked list data structure in Python from scratch. The key takeaways include:
- Circular linked lists connect the last node back to first forming a loop
- Must be careful when traversing to avoid infinite loops
- Useful for problems involving traversal, rotation and cycling
- Provides constant time insert and delete from head or tail
- Access and search is linear time since traversal is required
With the insert, delete and traversal methods implemented, you can now incorporate circular linked lists into your Python programs for seamless circular buffer and queueing operations. They open up applications in domains like multimedia, gaming and OS scheduling.
Hopefully this step-by-step explanation helps demystify this fundamental data structure! Circular linked lists strike an effective balance between arrays and normal linked lists providing efficient inserts/deletes while maintaining access to the first node. Add them to your Python coding toolbox today.