Introduction
Definition of linked list
Linked list is a linear data structure used to store a collection of elements. In contrast to arrays, linked lists do not have a fixed size and can grow or shrink as needed. Each element in a linked list, also known as a node, consists of a value and a pointer to the next node in the list.
Advantages of using linked lists
Dynamic size: Linked lists can be resized dynamically by adding or removing nodes. This makes linked lists a suitable data structure for situations where the size of the data is not known in advance.
Easy insertion and deletion: Insertion and deletion of elements in a linked list is easier and faster than in an array. This is because linked lists do not require shifting of elements to maintain the order.
Efficient memory usage: Linked lists only use as much memory as they need, whereas arrays have a fixed size and may waste memory if not fully utilized.
Implementation in C++
Creating a linked list in C++
To create a linked list in C++, we can define a class that represents a single node in the list. The class should have two data members: one to store the value of the node and another to store the address of the next node in the list.
Here's an example of a Node class in C++:
class Node {
public:
int data;
Node* next;
Node(int data) {
this->data = data;
this->next = NULL;
}
};
This class has two data members: data
to store the value of the node, and next
to store the address of the next node in the list. Note that next
is a pointer to a Node object, which allows us to create a chain of nodes that form the linked list.
We can now define a separate LinkedList class that contains methods to manipulate the nodes. Here's an example of a LinkedList class in C++:
class LinkedList {
private:
Node* head;
public:
LinkedList() {
head = NULL;
}
void insertAtBeginning(int value);
void insertAtEnd(int value);
void deleteFromBeginning();
void deleteFromEnd();
bool isEmpty();
void display();
};
This class has a private data member head
that points to the first node in the list. The class also contains methods to insert and delete nodes, check if the list is empty, and display the contents of the list.
Insertion and deletion operations in a linked list
Insertion
Insertion in a linked list involves creating a new node and inserting it at a specific position in the list. There are three main types of insertion operations:
Insertion at the beginning of the list
Insertion at the end of the list
Insertion at a specific position in the list
Let's take a look at how to implement each of these insertion operations.
Insertion at the Beginning of the List
To insert a new node at the beginning of a linked list, we simply need to create a new node and make it the new head of the list. Here's an implementation of this operation using the Node
class:
void insertAtBeginning(Node* &head, int data) {
Node* newNode = new Node(data);
newNode->next = head;
head = newNode;
}
This function takes in the head pointer of the linked list and the data to be inserted as arguments. It creates a new node with the given data and makes it the new head of the list by setting its next
pointer to the old head of the list and updating the head
pointer to point to the new node.
Insertion at the End of the List
To insert a new node at the end of a linked list, we need to traverse the entire list until we reach the last node, and then insert the new node after it. Here's an implementation of this operation using the Node
class:
void insertAtEnd(Node* &head, int data) {
Node* newNode = new Node(data);
if (head == NULL) {
head = newNode;
return;
}
Node* current = head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
This function takes in the head pointer of the linked list and the data to be inserted as arguments. It creates a new node with the given data and checks if the list is empty. If the list is empty, it sets the head
pointer to the new node and returns. Otherwise, it traverses the list until it reaches the last node and inserts the new node after it.
Insertion at a Specific Position in the List
To insert a new node at a specific position in a linked list, we need to traverse the list until we reach the position and then insert the new node after the previous node. Here's an implementation of this operation using the Node
class:
void insertAtPosition(Node* &head, int data, int position) {
Node* newNode = new Node(data);
if (position == 1) {
newNode->next = head;
head = newNode;
return;
}
Node* current = head;
int currentPosition = 1;
while (current != NULL && currentPosition < position - 1) {
current = current->next;
currentPosition++;
}
if (current == NULL) {
return;
}
newNode->next = current->next;
current->next = newNode;
}
This function takes in the head pointer of the linked list, the data to be inserted, and the position at which to insert the data as arguments. It creates a new node with the given data and checks if the position is the first position. If it is, it makes the new node the new head of the list. Otherwise, it traverses the list until it reaches the position before the insertion point and inserts the new node after it.
Deletion
Deletion in a linked list involves removing a node from the list. There are two main types of deletion operations:
Deletion of the first node in the list
Deletion of a node from a specific position in the list
Let's take a look at how to implement each of these deletion operations.
Deletion of the First Node in the List
To delete the first node in a linked list, we simply need to update the head
pointer to point to the second node in the list and delete the first node. Here's an implementation of this operation using the Node
class:
void deleteAtBeginning(Node* &head) {
if (head == NULL) {
return;
}
Node* temp = head;
head = head->next;
delete temp;
}
This function takes in the head pointer of the linked list as an argument. It checks if the list is empty, and if it is, it simply returns. Otherwise, it creates a temporary pointer to the first node, updates the head
pointer to point to the second node, and deletes the first node using the temporary pointer.
Deletion of a Node from a Specific Position in the List
To delete a node from a specific position in a linked list, we need to traverse the list until we reach the position and then delete the node at that position. Here's an implementation of this operation using the Node
class:
void deleteAtPosition(Node* &head, int position) {
if (head == NULL) {
return;
}
Node* current = head;
Node* previous = NULL;
int currentPosition = 1;
while (current != NULL && currentPosition < position) {
previous = current;
current = current->next;
currentPosition++;
}
if (current == NULL) {
return;
}
if (previous == NULL) {
head = current->next;
} else {
previous->next = current->next;
}
delete current;
}
This function takes in the head pointer of the linked list and the position of the node to be deleted as arguments. It checks if the list is empty, and if it is, it simply returns. Otherwise, it traverses the list until it reaches the position of the node to be deleted and deletes the node. It also updates the head
pointer if the first node is being deleted.
Doubly Linked List
A doubly linked list is a type of linked list in which each node has two pointers: one that points to the previous node in the list and one that points to the next node in the list. This allows for bi-directional traversal of the list. The first node in a doubly linked list is called the head, while the last node is called the tail.
Advantages of using a doubly linked list include:
Bi-directional traversal of the list, which can be useful in certain algorithms
More flexibility when it comes to inserting or deleting nodes, since we can easily traverse backwards as well as forwards
Implementation of a doubly linked list is similar to that of a singly linked list, except each node contains an additional pointer to the previous node. Here's an example implementation of a doubly linked list in C++:
class Node {
public:
int data;
Node* prev;
Node* next;
Node(int data) {
this->data = data;
prev = NULL;
next = NULL;
}
};
class DoublyLinkedList {
private:
Node* head;
Node* tail;
public:
DoublyLinkedList() {
head = NULL;
tail = NULL;
}
void insertAtBeginning(int data) {
Node* newNode = new Node(data);
if (head == NULL) {
head = newNode;
tail = newNode;
}
else {
newNode->next = head;
head->prev = newNode;
head = newNode;
}
}
void insertAtEnd(int data) {
Node* newNode = new Node(data);
if (tail == NULL) {
head = newNode;
tail = newNode;
}
else {
tail->next = newNode;
newNode->prev = tail;
tail = newNode;
}
}
void deleteFromBeginning() {
if (head == NULL) {
return;
}
Node* temp = head;
head = head->next;
if (head != NULL) {
head->prev = NULL;
}
delete temp;
}
void deleteFromEnd() {
if (tail == NULL) {
return;
}
Node* temp = tail;
tail = tail->prev;
if (tail != NULL) {
tail->next = NULL;
}
delete temp;
}
void display() {
Node* temp = head;
while (temp != NULL) {
cout << temp->data << " ";
temp = temp->next;
}
cout << endl;
}
};
The DoublyLinkedList
class contains a Node
class, which defines the structure of each node in the list. The DoublyLinkedList
class contains two pointers: head
points to the first node in the list, while tail
points to the last node in the list. The constructor initializes both pointers to NULL
.
The insertAtBeginning
method inserts a new node at the beginning of the list by creating a new Node
object with the given data
and setting the prev
pointer of the new node to NULL
. If the list is empty, the new node becomes both the head and tail of the list. Otherwise, the next
pointer of the new node is set to the current head
, the prev
pointer of the current head
is set to the new node, and the head
pointer is updated to point to the new node.
The insertAtEnd
method is similar to insertAtBeginning
, but inserts the new node at the end of the list.
The deleteFromBeginning
method removes the first node in the list by updating the head
pointer to point to the second node in the list (if there is one), updating the prev
pointer of the new head to NULL
, and deleting the original head node.
The deleteFromEnd
method removes the last node in the list by updating the tail
pointer to point to the second-to-last node in the list (if there is one), updating the next
pointer of the new tail to NULL
, and deleting the original tail node.
The display
method traverses the list from head
to tail
, printing the data
of each node as it goes.
With these methods, we can perform basic operations on a doubly linked list, such as inserting and deleting nodes, traversing the list.
In summary, a doubly linked list provides bi-directional traversal and more flexibility for insertion and deletion operations compared to a singly linked list. It can be implemented using a Node
class with prev
and next
pointers, and can be manipulated using methods such as insertAtBeginning
, insertAtEnd
, deleteFromBeginning
, deleteFromEnd
, anddisplay
.
Circular Linked List
A circular linked list is a type of linked list in which the last node of the list is connected to the first node, forming a circle. Unlike a singly linked list or a doubly linked list, a circular linked list has no end, it continues to loop through the nodes indefinitely.
Advantages of using circular linked list:
Circular linked lists can be useful in situations where data needs to be accessed in a circular manner, such as in a round-robin scheduling algorithm.
It allows for easy traversal of the list from any node, since there is no end to the list.
Circular linked lists can save memory by reusing the last node of the list as the first node.
C++ implementation for a circular linked list:
class Node {
public:
int data;
Node* next;
Node(int value) {
data = value;
next = NULL;
}
};
class CircularLinkedList {
private:
Node* head;
public:
CircularLinkedList() {
head = NULL;
}
void insertAtBeginning(int value) {
Node* newNode = new Node(value);
if (head == NULL) {
head = newNode;
newNode->next = head;
}
else {
Node* current = head;
while (current->next != head) {
current = current->next;
}
current->next = newNode;
newNode->next = head;
head = newNode;
}
}
void insertAtEnd(int value) {
Node* newNode = new Node(value);
if (head == NULL) {
head = newNode;
newNode->next = head;
}
else {
Node* current = head;
while (current->next != head) {
current = current->next;
}
current->next = newNode;
newNode->next = head;
}
}
void deleteFromBeginning() {
if (head == NULL) {
cout << "List is empty" << endl;
}
else if (head->next == head) {
head = NULL;
delete head;
}
else {
Node* current = head;
while (current->next != head) {
current = current->next;
}
Node* temp = head;
head = head->next;
current->next = head;
delete temp;
}
}
void deleteFromEnd() {
if (head == NULL) {
cout << "List is empty" << endl;
}
else if (head->next == head) {
head = NULL;
delete head;
}
else {
Node* current = head;
while (current->next->next != head) {
current = current->next;
}
Node* temp = current->next;
current->next = head;
delete temp;
}
}
void traverse() {
if (head == NULL) {
cout << "List is empty" << endl;
}
else {
Node* current = head;
do {
cout << current->data << " ";
current = current->next;
} while (current != head);
cout << endl;
}
}
};
This implementation defines two classes: Node
and CircularLinkedList
. The Node
class has a constructor that takes an integer value as input and sets the data
and next
pointer of the node object.
The CircularLinkedList
class has a head
pointer to the first node in the list. The class provides methods to insert a node at the beginning or end of the list, delete a node from the beginning or end of the list, traverse the list and print its elements.
Common Linked List Problems and Solutions
Linked lists are a commonly used data structure in programming, but they come with their own set of challenges. In this section of the blog, we will explore some common problems that can arise when working with linked lists and discuss solutions to those problems.
- Detecting loops using Floyd's cycle detection algorithm
One common problem with linked lists is that they can contain loops, where a node in the list points to a previous node in the list. Detecting these loops is important because they can cause infinite loops or other issues in programs that use the linked list.
Floyd's cycle detection algorithm is a popular solution to this problem. It works by using two pointers, one that moves through the list one node at a time (slow pointer) and another that moves through the list two nodes at a time (fast pointer). If there is a loop in the list, the fast pointer will eventually catch up to the slow pointer, indicating the presence of a loop.
bool detectLoop(Node* head) {
Node *slow = head, *fast = head;
while (slow && fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return true;
}
}
return false;
}
- Detecting and removing loops in a linked list
Once a loop is detected in a linked list, it is important to remove it to prevent any issues that may arise from it. One way to remove a loop is to break the cycle by setting the next pointer of the last node in the loop to NULL.
void detectAndRemoveLoop(Node* head) {
Node *slow = head, *fast = head;
while (slow && fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
break;
}
}
if (slow == head) {
while (fast->next != head) {
fast = fast->next;
}
fast->next = NULL;
} else if (slow == fast) {
slow = head;
while (slow->next != fast->next) {
slow = slow->next;
fast = fast->next;
}
fast->next = NULL;
}
}
- Finding the middle element of a linked list
To find the middle element of a linked list, we can use the "tortoise and hare" technique again. We start with two pointers, one that moves through the list one node at a time (tortoise) and another that moves through the list two nodes at a time (hare). When the hare reaches the end of the list, the tortoise will be at the middle node.
Node* findMiddle(Node* head) {
Node *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
- Reversing a linked list
Reversing a linked list is a common operation that involves changing the direction of the pointers in the list. This can be useful in many applications, such as when we need to traverse the list in reverse order.
To reverse a linked list, we can use a simple iterative approach that involves changing the next pointer of each node to point to its previous node. We also need to keep track of the previous and current nodes as we traverse the list.
Node* reverseList(Node* head) {
Node *prev = NULL, *curr = head, *next = NULL;
while (curr) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
Conclusion
Whew! That was quite a journey into the world of linked lists, wasn't it? From implementation in C++, to exploring the various types of linked lists, and tackling some common problems, we covered a lot of ground.
But what's the big deal about linked lists anyway? Well, they're like the superheroes of data structures – they save the day when arrays just won't cut it. With their dynamic allocation, efficient memory usage, and flexibility, linked lists are a powerful tool in any programmer's arsenal.
So, whether you're a seasoned developer or just starting out, don't shy away from linked lists. Embrace the challenge, roll up your sleeves, and get ready to level up your coding skills.
If you found this article helpful, don't forget to share it with your friends and colleagues. And if you liked what you read, be sure to hit that like button and leave a comment. Your support means the world to me.
Thank you for reading, and happy coding!