How to create a circular linked list with helper methods in C++

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 optionally tail) 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.

Circular singly linked list
Circular singly linked list
  • 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.

Circular doubly linked list
Circular doubly linked list

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.

Setting up the circular linked list in C++

Let's see how we can setup circular linked list in C++:

Node structure

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 node
Node* next; // Pointer to the next node
// Constructor to initialize a node
Node(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.

Circular linked list class

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 list
Node* tail; // Pointer to the last node in the circular linked list
public:
// Constructor
CircularLinkedList() : 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 empty
if (head != nullptr) {
Node* current = head; // Start at the head of the list
Node* nextNode;
// Traverse the circular linked list and delete each node
do {
nextNode = current->next; // Store pointer to the next node
delete current; // Delete the current node
current = 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 list
void AddToHead(int value);
// Adds a new node with the specified value at the tail of the list
void AddToTail(int value);
// Displays the contents of the list, starting from the head
void Display();
// Finds and returns a pointer to the first node containing the specified value
// Returns nullptr if the value is not found
Node* Find(int value);
// Removes the first node (head) from the list
// Updates head and tail pointers to maintain the circular structure
void RemoveHead();
// Removes the last node (tail) from the list
// Updates head and tail pointers to maintain the circular structure
void RemoveTail();
// Removes the first node containing the specified value
// Preserves the circular structure after removal
void RemoveByValue(int value);
};

Essential helper methods

Here are the essential helper methods for the circular linked list, each implemented to handle the circular nature of the list.

Adding nodes

  • 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 value
Node* newNode = new Node(value);
// Check if the list is empty
if (head == nullptr) {
// If empty, both head and tail point to the new node
head = newNode;
tail = newNode;
// Since it's a circular linked list, the new node points back to itself
newNode->next = head;
} else {
// If the list is not empty:
// Set the new node's next pointer to the current head
newNode->next = head;
// Update the head pointer to the new node
head = newNode;
// Update the tail's next pointer to maintain the circular structure
tail->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 value
Node* newNode = new Node(value);
// Check if the list is empty
if (head == nullptr) {
// If empty, both head and tail point to the new node
head = newNode;
tail = newNode;
// Since it's a circular linked list, the new node points back to itself
newNode->next = head;
} else {
// If the list is not empty:
// Set the current tail's next pointer to the new node
tail->next = newNode;
// Update the tail pointer to the new node
tail = newNode;
// Maintain the circular structure by pointing the new tail's next to the head
tail->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.

Traversing the list

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 empty
if (head == nullptr) {
// If empty, print a message and return
std::cout << "List is empty." << std::endl;
return;
}
// Start traversal from the head of the list
Node* current = head;
// Use a do-while loop to ensure every node is visited exactly once
do {
// Print the data of the current node
std::cout << current->data << " ";
// Move to the next node
current = current->next;
} while (current != head); // Stop when we return to the head node
// Print a newline to finalize the output
std::cout << std::endl;
}

Finding a node

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 empty
if (head == nullptr) {
// If the list is empty, return nullptr as no nodes exist
return nullptr;
}
// Start traversal from the head of the list
Node* current = head;
// Use a do-while loop to traverse the circular linked list
do {
// Check if the current node's data matches the given value
if (current->data == value) {
// If a match is found, return the pointer to the current node
return current;
}
// Move to the next node in the list
current = current->next;
} while (current != head); // Stop when we return to the head node
// If the value is not found after traversing the list, return nullptr
return nullptr;
}

Removing nodes

  • removeHead: Removes the head node from the list.

void CircularLinkedList::RemoveHead() {
// Check if the list is empty
if (head == nullptr) {
// If the list is empty, there is nothing to remove, so return
return;
}
// Check if the list has only one node
if (head == tail) {
// If there is only one node, delete it
delete head;
// Set both head and tail to nullptr, marking the list as empty
head = nullptr;
tail = nullptr;
} else {
// If the list has more than one node:
// Store the current head in a temporary pointer for deletion
Node* temp = head;
// Move the head pointer to the next node
head = head->next;
// Update the tail's next pointer to the new head, maintaining circular structure
tail->next = head;
// Delete the old head node to free memory
delete temp;
}
}
  • removeTail: Removes the last node from the list.

void CircularLinkedList::RemoveTail() {
// Check if the list is empty
if (head == nullptr) {
// If the list is empty, there is nothing to remove, so return
return;
}
// Check if the list has only one node
if (head == tail) {
// If there is only one node, delete it
delete head;
// Set both head and tail to nullptr, marking the list as empty
head = nullptr;
tail = nullptr;
} else {
// If the list has more than one node:
// Start at the head of the list
Node* current = head;
// Traverse the list to find the second-to-last node
while (current->next != tail) {
current = current->next;
}
// Update the second-to-last node's next pointer to point to the head
current->next = head;
// Delete the current tail node to free memory
delete tail;
// Update the tail pointer to the second-to-last node
tail = 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 empty
if (head == nullptr) {
// If the list is empty, there is nothing to remove, so return
return;
}
// Check if the value is in the head node
if (head->data == value) {
// Use the RemoveHead method to handle removing the head node
RemoveHead();
return;
}
// Start traversal from the head
Node* current = head;
do {
// Get a pointer to the next node
Node* nextNode = current->next;
// Check if the next node contains the value to be removed
if (nextNode->data == value) {
// Update the current node's next pointer to skip the node to be deleted
current->next = nextNode->next;
// Check if the node to be deleted is the tail
if (nextNode == tail) {
// Update the tail pointer to the current node
tail = current;
}
// Delete the node containing the value
delete nextNode;
// Return after successfully removing the node
return;
}
// Move to the next node in the list
current = 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).

Complete code

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.

main.cpp
CircularLinkedList.cpp
#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: 10
list.AddToHead(20);
std::cout << "Added 20 at head. List: ";
list.Display(); // Expected Output: 20 10
list.AddToTail(5);
std::cout << "Added 5 at tail. List: ";
list.Display(); // Expected Output: 20 10 5
std::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 5
std::cout << "\nRemoving the tail node:" << std::endl;
list.RemoveTail();
std::cout << "After removing tail, list: ";
list.Display(); // Expected Output: 10
std::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 30
list.AddToTail(40);
std::cout << "Added 40 at tail. List: ";
list.Display(); // Expected Output: 10 30 40
list.AddToTail(50);
std::cout << "Added 50 at tail. List: ";
list.Display(); // Expected Output: 10 30 40 50
std::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 50
std::cout << "\nAttempting to remove a node with non-existent value 100:" << std::endl;
list.RemoveByValue(100); // No effect
std::cout << "After attempting to remove 100 (not in list), list: ";
list.Display(); // Expected Output: 10 30 50
std::cout << "\nRemoving all nodes to empty the list:" << std::endl;
list.RemoveHead();
std::cout << "After removing head, list: ";
list.Display(); // Expected Output: 30 50
list.RemoveHead();
std::cout << "After removing head, list: ";
list.Display(); // Expected Output: 50
list.RemoveHead();
std::cout << "After removing head, list: ";
list.Display(); // Expected Output: List is empty.
return 0;
}
C++ Code for a Circular Singly Linked List

Complexity analysis

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

AddToHead

Insert at the beginning

O(1)

O(1)

Only modifies the head and tail pointers if necessary.

AddToTail

Insert at the end

O(1)

O(1)

Modifies the tail pointer and updates its next pointer to head.

Display

Traverse and print list

O(n)

O(1)

Traverses each node once, where n is the number of nodes in the list.

Find

Search for a value

O(n)

O(1)

Traverses the list until the node is found or reaches back to the head.

RemoveHead

Remove the head node

O(1)

O(1)

Removes the first node and updates the head and tail pointers if needed.

RemoveTail

Remove the tail node

O(n)

O(1)

Traverses the list to find the second-to-last node, then updates the tail pointer.

RemoveByValue

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.

Test your knowledge

CircularLinkedList

1

Which statement about circular singly linked lists is correct?

A)

Circular singly linked lists have a beginning and an end and each node points to the previous node.

B)

Circular singly linked lists have a beginning and an end and the last node points to the first node.

C)

Circular singly linked lists have only one node, which points to itself.

D)

Circular singly linked lists have a fixed size and cannot be dynamically resized.

Question 1 of 40 attempted

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!

Frequently asked questions

Haven’t found what you were looking for? Contact Us


How can you determine the number of nodes in a circular header linked list?

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.


Why do we use a circular linked list?

Circular linked lists are ideal for applications requiring continuous or cyclic data processing, like round-robin scheduling or buffering, as they naturally loop back to the start without needing to reset pointers.


What is the difference between linear and circular linked lists?

In a linear linked list, the last node points to null, ending the list. In a circular linked list, the last node points back to the head, creating a loop that allows continuous traversal.


What are the disadvantages of circular linked lists?

Circular linked lists can lead to infinite loops if traversal is not carefully controlled, and certain operations, like deletion, may be more complex due to the lack of a natural endpoint (null).


Free Resources

Copyright ©2024 Educative, Inc. All rights reserved