Skip to main content

Command Palette

Search for a command to run...

Linked Lists Explained: Navigate Your Dynamic Data Efficiently

Published
9 min read
Linked Lists Explained: Navigate Your Dynamic Data Efficiently
K

Hi, I'm Keerthi Ravilla Subramanyam, a passionate tech enthusiast with a Master's in Computer Science. I love diving deep into topics like Data Structures, Algorithms, and Machine Learning. With a background in cloud engineering and experience working with AWS and Python, I enjoy solving complex problems and sharing what I learn along the way. On this blog, you’ll find articles focused on breaking down DSA concepts, exploring AI, and practical coding tips for aspiring developers. I’m also on a journey to apply my skills in real-world projects like predictive maintenance and data analysis. Follow along for insightful discussions, tutorials, and code snippets to sharpen your technical skills.

In our journey through Data Structures and Algorithms (DSA), we’ve explored arrays and strings—static data structures that store elements in a continuous block of memory. Now, it’s time to introduce a more flexible data structure: the Linked List.

Linked lists allow dynamic memory allocation, meaning elements can be scattered in memory, and linked together using pointers. This flexibility makes linked lists ideal for efficient insertion and deletion operations, especially when compared to arrays. Let's break down how linked lists work. Firstly..,

What is a Linked List?

A Linked List is a linear data structure where each element (called a node) contains two parts:

  1. Data: The value of the element.

  2. Pointer/Reference: A link to the next node in the sequence.

Basic Terminologies of Linked List - GeeksforGeeks

Unlike arrays, linked lists do not store elements in contiguous memory locations. Instead, each element points to the next one, forming a chain. This allows linked lists to grow or shrink dynamically during runtime.

Types of Linked Lists

  1. Singly Linked List

    A Singly Linked List is the simplest form of a linked list where each node points to the next node, and the last node points to null. It's a one-way street!

    • Insertion: Adding a new node at the head, tail, or any position requires adjusting pointers, but there’s no need to shift elements like in arrays.

    • Deletion: Removing a node is as simple as redirecting the pointer from the previous node to the next one.

    • Traversal: You can only traverse from the head node to the tail in one direction.

Basic operations in a singly linked list:

    class Node:
        def __init__(self, data):
            # Initialize a new node with the given data and no next node
            self.data = data   # Store the data value in the node
            self.next = None   # Initialize the next pointer to None, indicating the end of the list

    class SinglyLinkedList:
        def __init__(self):
            # Initialize an empty linked list with no head node
            self.head = None  # The head of the list is set to None, indicating the list is empty

        # Insert a new node at the beginning of the list
        def insert_at_head(self, data):
            new_node = Node(data)  # Create a new node with the given data
            new_node.next = self.head  # Point the new node's next to the current head
            self.head = new_node  # Update the head to be the new node

        # Delete the first occurrence of a given value from the list
        def delete_node(self, key):
            temp = self.head  # Start from the head of the list

            # Check if the list is not empty and if the head needs to be deleted
            if temp is not None:
                if temp.data == key:  # If the head node holds the value to be deleted
                    self.head = temp.next  # Update the head to the next node
                    temp = None  # Remove the reference to the deleted node
                    return  # Exit the method since the node has been deleted

            # Traverse the list to find the node to delete
            while temp is not None:
                if temp.data == key:  # Check if the current node holds the value to be deleted
                    break  # Exit the loop if the node is found
                prev = temp  # Keep track of the previous node
                temp = temp.next  # Move to the next node

            # If we reached the end of the list without finding the key
            if temp is None:
                return  # Exit since the value was not found in the list

            # Remove the node from the list
            prev.next = temp.next  # Link the previous node to the node after the current node
            temp = None  # Remove the reference to the deleted node
  1. Doubly Linked List

    In a Doubly Linked List, each node contains two pointers—one that points to the next node and one that points to the previous node. This allows traversal in both directions.

    • Bidirectional traversal: Unlike the singly linked list, you can traverse both forward and backward.

    • Insertion and deletion: Similar to singly linked lists but require adjusting two pointers instead of one.

Basic operations in a doubly linked list:

    class Node:
        def __init__(self, data):
            # Initialize a new node with the given data and no previous or next node
            self.data = data   # Store the data value in the node
            self.prev = None   # Initialize the previous pointer to None, indicating no previous node
            self.next = None   # Initialize the next pointer to None, indicating no next node

    class DoublyLinkedList:
        def __init__(self):
            # Initialize an empty doubly linked list with no head node
            self.head = None  # The head of the list is set to None, indicating the list is empty

        # Insert a new node at the beginning of the list
        def insert_at_head(self, data):
            new_node = Node(data)  # Create a new node with the given data
            if self.head is not None:  # If the list is not empty
                self.head.prev = new_node  # Set the previous pointer of the current head to the new node
            new_node.next = self.head  # Point the new node's next to the current head
            self.head = new_node  # Update the head to be the new node
            new_node.prev = None  # The new head's previous node should be None

        # Delete a node with a given value from the list
        def delete_node(self, key):
            temp = self.head  # Start from the head of the list

            # Traverse the list to find the node to delete
            while temp is not None:
                if temp.data == key:  # Check if the current node holds the value to be deleted
                    # Node found; perform deletion
                    if temp.prev is not None:
                        temp.prev.next = temp.next  # Link the previous node to the next node
                    if temp.next is not None:
                        temp.next.prev = temp.prev  # Link the next node to the previous node
                    if temp == self.head:  # If the node to delete is the head
                        self.head = temp.next  # Update the head to the next node
                    temp = None  # Remove the reference to the deleted node
                    return  # Node deleted; exit function
                temp = temp.next  # Move to the next node

        # Print the list in a readable format
        def print_list(self):
            temp = self.head  # Start from the head of the list
            while temp:  # Traverse until the end of the list
                print(temp.data, end=' <-> ')  # Print the current node's data
                temp = temp.next  # Move to the next node
            print('None')  # Indicate the end of the list
  1. Circular Linked List

    In a Circular Linked List, the last node points back to the first node, creating a circular structure. Circular linked lists can either be singly or doubly linked. In a circular linked list:

    • The last node's next pointer points to the first node, forming a closed loop.

    • It can either be singly or doubly linked, depending on whether nodes point only to the next node or to both the next and previous nodes.

This structure is particularly useful in applications that require repeated cycling through a sequence, such as implementing a round-robin scheduling algorithm.

Basic operations in circular linked list:

class Node:
    def __init__(self, data):
        # Initialize a new node with the given data and no next node
        self.data = data   # Store the data value in the node
        self.next = None   # Initialize the next pointer to None, indicating no next node

class CircularLinkedList:
    def __init__(self):
        # Initialize an empty circular linked list with no head node
        self.head = None  # The head of the list is set to None, indicating the list is empty

    # Insert a new node at the end of the circular linked list
    def insert(self, data):
        new_node = Node(data)  # Create a new node with the given data
        if not self.head:  # If the list is empty
            self.head = new_node  # Set the new node as the head
            new_node.next = self.head  # Point the new node's next to itself (circular link)
        else:
            temp = self.head
            while temp.next != self.head:  # Traverse till the last node
                temp = temp.next
            temp.next = new_node  # Last node's next becomes the new node
            new_node.next = self.head  # New node points back to the head

    # Delete a node by its value from the circular linked list
    def delete(self, key):
        if self.head is None:  # If the list is empty, nothing to delete
            return

        curr = self.head
        prev = None

        # If the node to be deleted is the head
        if curr.data == key:
            if curr.next == self.head:  # Only one node in the list
                self.head = None  # Set head to None, indicating the list is now empty
            else:
                while curr.next != self.head:  # Traverse to the last node
                    curr = curr.next
                curr.next = self.head.next  # Last node points to the second node
                self.head = self.head.next  # Update head to the next node
            return

        # Deleting a non-head node
        curr = self.head
        while curr.next != self.head and curr.data != key:  # Traverse the list to find the node
            prev = curr
            curr = curr.next

        if curr.data == key:  # If the node to delete is found
            prev.next = curr.next  # Bypass the current node
        else:
            print(f"Node with value {key} not found")  # If the node is not found

    # Traverse and print the circular linked list
    def print_list(self):
        if not self.head:  # If the list is empty
            print("List is empty")
            return

        temp = self.head
        while True:  # Start from the head and loop through the list
            print(temp.data, end=" -> ")  # Print the current node's data
            temp = temp.next  # Move to the next node
            if temp == self.head:  # Stop once we circle back to the head
                break
        print()  # Print a newline at the end

Advantages and disadvantages of Linked Lists

AdvantagesDisadvantages
Dynamic Size: Linked Lists can grow and shrink in size dynamically, allowing for efficient memory usage.Memory Overhead: Each node in a linked list requires extra memory for a pointer/reference.
Efficient Insertions/Deletions: Adding or removing nodes can be done in constant time (O(1)) if the pointer/reference to the location is known.No Direct Access: Elements must be accessed sequentially, leading to O(n) time complexity for searching.
No Predefined Size: Unlike arrays, linked lists do not require a fixed size at initialization.Cache Locality: Linked lists do not have good locality of reference compared to arrays, which can affect performance.
Flexibility: Linked lists can easily implement complex data structures like stacks, queues, and graphs.Complexity: Implementation and manipulation of linked lists can be more complex than arrays.
Circular Linked Lists allow traversal from any node to any other node, which can be useful in certain applications.Potential Memory Leaks: If nodes are not properly deallocated, it can lead to memory leaks, especially in languages without garbage collection.

Common Use Cases for Linked Lists

  • Implementation of stacks and queues.

  • Undo functionality in applications like text editors (using doubly linked lists).

  • Circular buffering (using circular linked lists).

Essential Resources for Mastering Linked Lists

Conclusion

Linked lists are an essential data structure for anyone diving into DSA. Their flexibility and efficiency make them suitable for dynamic scenarios, especially where frequent insertions and deletions are needed. However, the trade-off comes in terms of memory overhead and slower access times compared to arrays.

Next, we’ll explore more advanced data structures that build on these concepts, but for now, mastering linked lists is a crucial step in understanding the underlying mechanics of many algorithms and systems.

Data structures course

Part 5 of 14

🌟Explore the world of Data Structures and Algorithms in this series! Each article simplifies key concepts, and real-world applications. Perfect for beginners and seasoned developers alike. Let's enhance our coding skills!🚀

Up next

A Simple Explanation of Stacks: The Last-In-First-Out Method

In our exploration of Data Structures and Algorithms (DSA), we've looked at how different structures work, from arrays to linked lists. Today, we'll focus on other basic fundamental structures stacks. Stacks work on the Last-In-First-Out (LIFO) princ...

More from this blog

K

Keerthi's Dev Chronicles

26 posts

Welcome to Keerthi's Coding Blogs! Explore how Data Structures and Algorithms enhance cloud solutions on AWS. Join me to discover insights and tips. Let’s connect and learn together!