Start at the head node and traverse the list, counting each node until you return to the head. This gives the total number of nodes in the list.
Key takeaways:
A circular linked list differs from other lists in that the last node points back to the head, creating a loop. This structure allows continuous traversal from any point in the list.
Each node in a circular linked list contains data and a pointer to the next node. The list class includes essential pointers (
head
and optionallytail
) for managing the list efficiently.Essential operations like adding nodes (to the head or tail), traversing, finding, and removing nodes are straightforward and leverage the circular structure. Each method is designed to maintain the looped nature of the list.
Adding or removing nodes from the head or tail can often be done in O(1) time, while traversal and finding a node take O(n) time. All operations use constant space, making circular linked lists efficient in memory usage.
Circular linked lists are well-suited for applications like round-robin scheduling, cyclic buffers, and multiplayer games, where cyclic data processing is beneficial.
Mastering circular linked lists is a foundation for exploring more advanced data structures like doubly circular linked lists and circular queues, which build on similar principles.
Linked lists are one of the fundamental data structures in computer science. They consist of nodes, each containing data and a pointer to the next node, creating a sequence of elements. Unlike arrays, linked lists do not require a contiguous block of memory, which makes them more flexible for dynamic data storage. They allow efficient insertion and deletion operations, especially when the size of the data set is unknown in advance.
A circular linked list is a special type of linked list where the last node points back to the first node, forming a circular structure. This can be further categorized as follows:
Singly circular linked list: Each node points to the next node, and the last node points back to the first node.
Doubly circular linked list: Each node has two pointers, one to the next node and one to the previous node, with the last node's "next" pointer pointing to the head and the head's "previous" pointer pointing to the last node.
This circular structure eliminates the need for a "null" pointer at the end, allowing continuous traversal from any point in the list. It also enables more efficient looping through data, as the list can be navigated back to the start seamlessly.
Note: Circular linked lists eliminate the need for special handling at the end of the list, as every node points to the next in a continuous loop. This makes them particularly useful in scenarios where you need to process data in a cycle, such as round-robin scheduling.
Let's see how we can setup circular linked list in C++:
To start, we define the structure of a node that will be used in our circular linked list. Each node will contain:
A data
field to store the node’s value.
A next
pointer to reference the next node in the list.
Here’s the basic structure of a node:
struct Node {int data; // Value of the nodeNode* next; // Pointer to the next node// Constructor to initialize a nodeNode(int value) : data(value), next(nullptr) {}};
In a circular linked list, the next
pointer of the last node will eventually point back to the head node, creating a loop in the list.
Next, we define the class for our circular linked list. This class will include:
A head
pointer, which points to the first node in the list. If the list is empty, head
will be nullptr
.
A tail
pointer (optional, but useful for certain operations), which can point to the last node, allowing efficient insertion at the end.
Here’s the basic structure of the circular linked list class:
class CircularLinkedList {private:Node* head; // Pointer to the first node in the circular linked listNode* tail; // Pointer to the last node in the circular linked listpublic:// ConstructorCircularLinkedList() : head(nullptr), tail(nullptr) {// Initializes an empty list by setting head and tail to nullptr}// Destructor to clean up resources~CircularLinkedList() {// Check if the list is not emptyif (head != nullptr) {Node* current = head; // Start at the head of the listNode* nextNode;// Traverse the circular linked list and delete each nodedo {nextNode = current->next; // Store pointer to the next nodedelete current; // Delete the current nodecurrent = nextNode; // Move to the next node} while (current != head); // Stop when we loop back to the head}}// Adds a new node with the specified value at the head of the listvoid AddToHead(int value);// Adds a new node with the specified value at the tail of the listvoid AddToTail(int value);// Displays the contents of the list, starting from the headvoid Display();// Finds and returns a pointer to the first node containing the specified value// Returns nullptr if the value is not foundNode* Find(int value);// Removes the first node (head) from the list// Updates head and tail pointers to maintain the circular structurevoid RemoveHead();// Removes the last node (tail) from the list// Updates head and tail pointers to maintain the circular structurevoid RemoveTail();// Removes the first node containing the specified value// Preserves the circular structure after removalvoid RemoveByValue(int value);};
Here are the essential helper methods for the circular linked list, each implemented to handle the circular nature of the list.
addToHead
: Inserts a new node at the beginning of the circular linked list.
void CircularLinkedList::AddToHead(int value) {// Create a new node with the given valueNode* newNode = new Node(value);// Check if the list is emptyif (head == nullptr) {// If empty, both head and tail point to the new nodehead = newNode;tail = newNode;// Since it's a circular linked list, the new node points back to itselfnewNode->next = head;} else {// If the list is not empty:// Set the new node's next pointer to the current headnewNode->next = head;// Update the head pointer to the new nodehead = newNode;// Update the tail's next pointer to maintain the circular structuretail->next = head;}}
addToTail
: Inserts a new node at the end of the circular linked list.
void CircularLinkedList::AddToTail(int value) {// Create a new node with the given valueNode* newNode = new Node(value);// Check if the list is emptyif (head == nullptr) {// If empty, both head and tail point to the new nodehead = newNode;tail = newNode;// Since it's a circular linked list, the new node points back to itselfnewNode->next = head;} else {// If the list is not empty:// Set the current tail's next pointer to the new nodetail->next = newNode;// Update the tail pointer to the new nodetail = newNode;// Maintain the circular structure by pointing the new tail's next to the headtail->next = head;}}
Note: Adding nodes to the head or tail in a circular linked list is efficient because it doesn’t require shifting or resizing, unlike arrays. This makes circular linked lists particularly advantageous for dynamic data management.
To print all the nodes in the circular linked list, we need to consider the circular nature to avoid infinite looping.
void CircularLinkedList::Display() {// Check if the list is emptyif (head == nullptr) {// If empty, print a message and returnstd::cout << "List is empty." << std::endl;return;}// Start traversal from the head of the listNode* current = head;// Use a do-while loop to ensure every node is visited exactly oncedo {// Print the data of the current nodestd::cout << current->data << " ";// Move to the next nodecurrent = current->next;} while (current != head); // Stop when we return to the head node// Print a newline to finalize the outputstd::cout << std::endl;}
This method searches for a node by its value. If the node is found, it returns a pointer to the node; otherwise, it returns nullptr
.
Node* CircularLinkedList::Find(int value) {// Check if the list is emptyif (head == nullptr) {// If the list is empty, return nullptr as no nodes existreturn nullptr;}// Start traversal from the head of the listNode* current = head;// Use a do-while loop to traverse the circular linked listdo {// Check if the current node's data matches the given valueif (current->data == value) {// If a match is found, return the pointer to the current nodereturn current;}// Move to the next node in the listcurrent = current->next;} while (current != head); // Stop when we return to the head node// If the value is not found after traversing the list, return nullptrreturn nullptr;}
removeHead
: Removes the head node from the list.
void CircularLinkedList::RemoveHead() {// Check if the list is emptyif (head == nullptr) {// If the list is empty, there is nothing to remove, so returnreturn;}// Check if the list has only one nodeif (head == tail) {// If there is only one node, delete itdelete head;// Set both head and tail to nullptr, marking the list as emptyhead = nullptr;tail = nullptr;} else {// If the list has more than one node:// Store the current head in a temporary pointer for deletionNode* temp = head;// Move the head pointer to the next nodehead = head->next;// Update the tail's next pointer to the new head, maintaining circular structuretail->next = head;// Delete the old head node to free memorydelete temp;}}
removeTail
: Removes the last node from the list.
void CircularLinkedList::RemoveTail() {// Check if the list is emptyif (head == nullptr) {// If the list is empty, there is nothing to remove, so returnreturn;}// Check if the list has only one nodeif (head == tail) {// If there is only one node, delete itdelete head;// Set both head and tail to nullptr, marking the list as emptyhead = nullptr;tail = nullptr;} else {// If the list has more than one node:// Start at the head of the listNode* current = head;// Traverse the list to find the second-to-last nodewhile (current->next != tail) {current = current->next;}// Update the second-to-last node's next pointer to point to the headcurrent->next = head;// Delete the current tail node to free memorydelete tail;// Update the tail pointer to the second-to-last nodetail = current;}}
removeByValue
: Removes a node by its value. If the node is found, it is removed; if not, the list remains unchanged.
void CircularLinkedList::RemoveByValue(int value) {// Check if the list is emptyif (head == nullptr) {// If the list is empty, there is nothing to remove, so returnreturn;}// Check if the value is in the head nodeif (head->data == value) {// Use the RemoveHead method to handle removing the head nodeRemoveHead();return;}// Start traversal from the headNode* current = head;do {// Get a pointer to the next nodeNode* nextNode = current->next;// Check if the next node contains the value to be removedif (nextNode->data == value) {// Update the current node's next pointer to skip the node to be deletedcurrent->next = nextNode->next;// Check if the node to be deleted is the tailif (nextNode == tail) {// Update the tail pointer to the current nodetail = current;}// Delete the node containing the valuedelete nextNode;// Return after successfully removing the nodereturn;}// Move to the next node in the listcurrent = current->next;} while (current != head); // Stop when we loop back to the head}
These helper methods provide a full range of functionalities for adding, finding, and removing nodes in the circular linked list while preserving the circular structure. Each method is designed to handle both typical cases and edge cases (like an empty list or a single-node list).
This is the complete implementation of the CircularLinkedList class in C++ with all the helper methods discussed earlier. The accompanying main.cpp
file includes examples and test cases to demonstrate the functionality of each method, while CircularLinkedList.cpp
contains the method definitions for better modularity and readability.
#include "CircularLinkedList.cpp"#include <iostream>int main() {CircularLinkedList list;std::cout << "Adding nodes to the list:" << std::endl;list.AddToHead(10);std::cout << "Added 10 at head. List: ";list.Display(); // Expected Output: 10list.AddToHead(20);std::cout << "Added 20 at head. List: ";list.Display(); // Expected Output: 20 10list.AddToTail(5);std::cout << "Added 5 at tail. List: ";list.Display(); // Expected Output: 20 10 5std::cout << "\nFinding a node with value 10:" << std::endl;Node* foundNode = list.Find(10);if (foundNode) {std::cout << "Node with value 10 found." << std::endl; // Expected output} else {std::cout << "Node with value 10 not found." << std::endl;}std::cout << "\nRemoving the head node:" << std::endl;list.RemoveHead();std::cout << "After removing head, list: ";list.Display(); // Expected Output: 10 5std::cout << "\nRemoving the tail node:" << std::endl;list.RemoveTail();std::cout << "After removing tail, list: ";list.Display(); // Expected Output: 10std::cout << "\nAdding more nodes to test removal by value:" << std::endl;list.AddToTail(30);std::cout << "Added 30 at tail. List: ";list.Display(); // Expected Output: 10 30list.AddToTail(40);std::cout << "Added 40 at tail. List: ";list.Display(); // Expected Output: 10 30 40list.AddToTail(50);std::cout << "Added 50 at tail. List: ";list.Display(); // Expected Output: 10 30 40 50std::cout << "\nRemoving a node with value 40:" << std::endl;list.RemoveByValue(40);std::cout << "After removing node with value 40, list: ";list.Display(); // Expected Output: 10 30 50std::cout << "\nAttempting to remove a node with non-existent value 100:" << std::endl;list.RemoveByValue(100); // No effectstd::cout << "After attempting to remove 100 (not in list), list: ";list.Display(); // Expected Output: 10 30 50std::cout << "\nRemoving all nodes to empty the list:" << std::endl;list.RemoveHead();std::cout << "After removing head, list: ";list.Display(); // Expected Output: 30 50list.RemoveHead();std::cout << "After removing head, list: ";list.Display(); // Expected Output: 50list.RemoveHead();std::cout << "After removing head, list: ";list.Display(); // Expected Output: List is empty.return 0;}
Here's a table outlining the time and space complexity for each helper method in the CircularLinkedList
class.
Method | Operation | Time Complexity | Space Complexity | Explanation |
| Insert at the beginning | O(1) | O(1) | Only modifies the |
| Insert at the end | O(1) | O(1) | Modifies the |
| Traverse and print list | O(n) | O(1) | Traverses each node once, where n is the number of nodes in the list. |
| Search for a value | O(n) | O(1) | Traverses the list until the node is found or reaches back to the |
| Remove the head node | O(1) | O(1) | Removes the first node and updates the |
| Remove the tail node | O(n) | O(1) | Traverses the list to find the second-to-last node, then updates the |
| Remove a node by its value | O(n) | O(1) | Traverses the list to find the node with the specified value and removes it if found. |
CircularLinkedList
Which statement about circular singly linked lists is correct?
Circular singly linked lists have a beginning and an end and each node points to the previous node.
Circular singly linked lists have a beginning and an end and the last node points to the first node.
Circular singly linked lists have only one node, which points to itself.
Circular singly linked lists have a fixed size and cannot be dynamically resized.
Pro tip: Boost your data structure skills with the C++ course on Data Structures for Coding Interviews. It offers hands-on practice, in-depth explanations, and focuses on essential concepts frequently tested in interviews, including circular linked lists. Build a strong foundation to excel in coding interviews!
Haven’t found what you were looking for? Contact Us
Free Resources