Linked Lists 101: Everything You Need to Know

Linked Lists 101: Everything You Need to Know

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

  1. 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.

  2. 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.

  3. 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:

  1. Insertion at the beginning of the list

  2. Insertion at the end of the list

  3. 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:

  1. Deletion of the first node in the list

  2. 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!