Circular Linked Lists in Python

Circular Linked List Python Title

Circular Linked Lists are data structures that are used to store lists. It is very similar to linked lists but with a few extra features. In this tutorial, we will discuss what a circular linked list is, we will implement it in python and see its output.

Pre-Requisite: Understanding of Linked List

We must first define linked lists before moving on to circular linked lists.

A linked list is a list in which the list items are linked to other list items in a specific way. Different forms of linked lists have different ways of linking objects.

The “singly linked list” or simply “linked list” is the most popular linked list, in which each item links to the next item in the list. So, in order to get to the tenth item, we must first get to the ninth item, which is linked to the tenth item. And once we’ve accessed the tenth item, we’ll be able to access the eleventh item through the tenth item’s connection.

A node is a name given to each object in a linked list. Each node in a singly linked list has two components. The first part contains the node’s data, while the second contains the link to the next node.

Let’s take a look at circular linked lists now.

Circular Linked Lists in Python

A circular linked list is similar to a linked list in that the nodes are connected by links, but the last node is also linked to the first node instead of just linking to nothing. So, after we’ve accessed the last node, we can access the first node through the last node.

The way to do this is just instead of keeping the link of the last node as None, make it point to the first node instead.

The benefit of doing this is that it makes it easier to implement algorithms that have a list of items that come in a circular manner. For example, the round-robin scheduling algorithm, or the player turns in a multiplayer game are circular in nature.

To visualize, a circular linked list looks something like this:

Circular Linked List Repr
Circular Linked List Representation

In the above example, you can see that there are four nodes in the list. Each node has some data, and each node links to the next node of the list except the last node which links to the first node of the list.

There’s a head that points to the start of the list which is used to enter the list and iterate through the circular linked list.

Recommended read – Doubly linked lists

Implementing Circular Linked Lists in Python

To create a circular linked list, we create two classes: the first one for nodes and the second one for the linked list that will use the nodes.

Class: Node

For the node class, we have two members. One to store data and the other to store the link to the next node. The class definition will be:

class Node:
    def __init__(self, data = None):
        self.data = data
        self.next = self

So, initially, every new node created will either have or won’t have a data value depending on how it was created, but it will point to itself by default so that it’s like a single item circular linked list.

Class: Circular Linked List

This class will use nodes created by the previous class to implement a circular linked list. It will contain one head node, one count member, and multiple methods for specific tasks.

class CLL:
    def __init__(self):
        self.head = None
        self.count = 0
    
    def __repr__(self):
        string = ""
         
        if(self.head == None):
            string += "Circular Linked List Empty"
            return string
         
        string += f"Circular Linked List:\n{self.head.data}"       
        temp = self.head.next
        while(temp != self.head):
            string += f" -> {temp.data}"
            temp = temp.next
        return string
    
    def append(self, data):
        self.insert(data, self.count)
        return
    
    def insert(self, data, index):
        if (index > self.count) | (index < 0):
            raise ValueError(f"Index out of range: {index}, size: {self.count}")
            
        if self.head == None:
            self.head = Node(data)
            self.count += 1
            return
        
        temp = self.head
        for _ in range(self.count - 1 if index - 1 == -1 else index - 1):
            temp = temp.next
            
        aftertemp = temp.next #New node goes between temp and aftertemp
        temp.next = Node(data)
        temp.next.next = aftertemp
        if(index == 0):
            self.head = temp.next
        self.count += 1
        return
    
    def remove(self, index):
        if (index >= self.count) | (index < 0):
            raise ValueError(f"Index out of range: {index}, size: {self.count}")
        
        if self.count == 1:
            self.head = None
            self.count = 0
            return
        
        before = self.head
        for _ in range(self.count - 1 if index - 1 == -1 else index - 1):
            before = before.next
        after = before.next.next
        
        before.next = after
        if(index == 0):
            self.head = after
        self.count -= 1
        return
    
    def index(self, data):
        temp = self.head
        for i in range(self.count):
            if(temp.data == data):
                return i
            temp = temp.next
        return None
    
    def size(self):
        return self.count
    
    def display(self):
        print(self)

Let us discuss the methods written above.

The __init__ method

In the constructor, we are initializing two members, we are setting the head as None because there are no nodes in the list, and we are setting the count as 0 for the same reason.

The __repr__ method

The string that will print the linked list will be returned by the __repr__ process. So either the list is empty, in which case we print that, or the list isn’t empty, in which case we print each node’s data one by one.

The append and insert method

Nodes can either be appended or inserted at a specified position in this implementation. To append, we simply call the insert method and send the size of the list as the index.

In the insert method, we first check if the specified index is valid or not, if not, we throw a ValueError. After passing the check, if the list is empty, we simply assign the new node to the head, increment the count, and return from the method.

If the list isn’t empty, we first reach the node before the specified index. For example, if the given index is 5, then we reach the node at the 4th index, and because the list is circular, if the given index is 0, then we reach the last node of the list.

Now, we assign the new node to the next of the node before the specified index, and we make the new node’s next link to the node at the specified index. This will make sure that the new node is inserted before the node that was at the specified index, and hence taken its index and pushed it ahead.

Now, if the given index was 0, we have inserted a node after the last node of the list, so we simply make the head point to the new node making it the new head of the list.

The remove method

To remove an item we must specify where the item is to be removed from. If the specified index is out of range, we raise a ValueError. If there’s only one item on the list, we simply make the head None and the count 0, and return from the method.

Otherwise, we have to reach the node before the specified index and the node after the specified index. For example, if the specified index is 4 then we need to reach the 3rd node and the 5th node, and because the list is circular if the specified index is 0, we need to reach the last node (before it) and the 1st node (after it).

After this, we simply assign the node after the specified index to the next of the node before the specified index. This will skip the node at the specified index, hence removing it from the list. If the specified index is 0, then the head has been removed from the list, so we simply have to assign the node that was after the specified index to the head and the list will be restored. Don’t forget to decrement the count of the list.

The index, size and display method

The index method searches for an item in the list. If found, it returns its index, otherwise, it returns None. The size method returns the number of nodes in the list, and the display method prints the list.

The Output

Circular Linked List Output Append Insert
Example illustrating initialization, appending, and insertion.
Circular Linked List Output Remove Index Size Display
Example illustrating removal, searching, the size method, and printing.

Conclusion

We learned about Circular Linked Lists and how to use them in Python. We began by looking at how a singly linked list works, then moved on to how a circular linked list differs. We wrote the data structure code in Python, discussed how each method works, and then looked at the results of the code.

I hope you had a great time learning, and see you in the next tutorial.