Understanding Linked Lists

Understanding Linked Lists

Introduction

Insertion Operations in a Linked List

Fundamental data structures used in computer science and programming include linked lists. They offer a dynamic method of manipulating and storing data, enabling effective insertion and deletion operations. In this article, we'll look at how a linked list is implemented, including how to add items, show them, delete them, add items at the beginning, and determine whether the list is empty. We will also go over the time-related challenges of each procedure. We will provide real-world examples that demonstrate linked lists in action to help with comprehension.

Understanding Linked Lists

A linked list is a linear data structure consisting of a sequence of nodes. Each node contains data and a reference (or link) to the next node in the list. The last node typically points to null, indicating the end of the list. Unlike arrays, linked lists do not require contiguous memory allocation, making them flexible for dynamically changing data.

What is Linked List Data Structure ? | Arrays vs Linked List | Operations |  Types | Applications - Simple Snippets

There are different types of linked lists, including singly linked lists, doubly linked lists, and circular linked lists.

  • In a singly linked list, each node contains a reference only to the next node.

  • In a doubly linked list, each node has references to both the next and previous nodes, enabling traversal in both directions.

  • A circular linked list forms a loop where the last node points back to the first node, creating a circular structure.

Node Structure

To implement a linked list, we start by defining a node structure. Each node contains two components

  • The data

  • The next pointer. The next pointer points to the next node in the list

      class Node:
          def __init__(self, data):
              self.data = data
              self.next = None
    

Creating a Linked List

We must initialise the head (or start) pointer, which points to the list's very first node, to establish a linked list. The head pointer is first set to null since the list is empty. Let's examine the process for adding elements to a linked list.

  • Insertion at the Beginning

    To insert an element at the beginning of the list, we create a new node and make it the head node by updating the head pointer. The next pointer of the new node will point to the previous head node.

      def insert_at_beginning(head, data):
          new_node = Node(data)
          new_node.next = head
          head = new_node
          return head
    
  • Insertion / Insertion at the End

    We navigate the list until we find the final node, and then we place an element there. Next, we build a new node and change the last node's next pointer to point at the new node.

      def insert_at_end(head, data):
          new_node = Node(data)
          if head is None:
              head = new_node
          else:
              current = head
              while current.next:
                  current = current.next
              current.next = new_node
          return head
    
  • Deletion

    To remove a node from a linked list, we must modify the previous node's next reference so that it skips the node that will be removed. The memory is then released from the removed node. How to remove a node with a certain data value is shown below.

      def delete_node(head, data):
          if head is None:
              return head
    
          if head.data == data:
              head = head.next
              return head
    
          current = head
          while current.next:
              if current.next.data == data:
                  current.next = current.next.next
                  break
              current = current.next
    
          return head
    
  • Display

    To display the elements of a linked list, we traverse the list and print the data of each node.

      def display_list(head):
          current = head
          while current:
              print(current.data, end="->")
              current = current.next
          print()
    
  • Checking if the List is Empty

    To determine whether a linked list is empty, we check if the head pointer is null. If the head is null, the list is empty.

      def is_empty(head):
          return head is None
    

Time Complexities

  • Insertion at the Beginning: O(1)

  • Insertion at the End: O(n)

  • Deletion: O(n)

  • Displaying the List: O(n)

  • Checking if the List is Empty: O(1)

Real-time Example: To-Do List

Let's look at a practical example of how to develop a linked list to build a straightforward to-do list application. Users of the application can add tasks, mark them as finished, and view the list of tasks.

Implementation

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None


class ToDoList:
    def __init__(self):
        self.head = None

    def add_task(self, task):
        new_node = Node(task)
        if self.head is None:
            self.head = new_node
        else:
            current = self.head
            while current.next:
                current = current.next
            current.next = new_node

    def mark_as_completed(self, task):
        current = self.head
        while current:
            if current.data == task:
                current.data = f"{task} (Completed)"
                break
            current = current.next

    def display_tasks(self):
        current = self.head
        if current is None:
            print("No tasks in the list.")
        else:
            print("Tasks:")
            while current:
                print(current.data)
                current = current.next

Usage/Console

# Create an instance of the ToDoList
my_todo_list = ToDoList()

# Add tasks to the list
my_todo_list.add_task("Buy groceries")
my_todo_list.add_task("Finish report")
my_todo_list.add_task("Go for a run")

# Display the tasks
my_todo_list.display_tasks()
# Output:
# Tasks:
# Buy groceries
# Finish report
# Go for a run

# Mark a task as completed
my_todo_list.mark_as_completed("Finish report")

# Display the tasks after marking one as completed
my_todo_list.display_tasks()
# Output:
# Tasks:
# Buy groceries
# Finish report (Completed)
# Go for a run

In this example, a linked list has been used to implement a ToDoList class. Each task is represented by a node in the linked list, and the node's data property contains the task description. The ToDoList class's head attribute points to the list's root node.

A new task is added to the end of the linked list using the add_task method. A specific task is sought by the mark_as_completed method, which then modifies the task's description to reflect completion. The linked list is iterated by the display_tasks method, which writes out a description for each task.

This implementation allows users to maintain and manage their to-do lists dynamically. The flexibility of linked lists enables efficient insertion and deletion of tasks, making it suitable for scenarios where the task order changes frequently or new tasks are added dynamically.

Conclusion

Linked lists are adaptable data structures that facilitate quick insertion and removal. They offer flexibility in the administration of dynamic data. In this post, we looked at how linked lists are implemented, including how they are added to, removed from, shown, and checked to see if they are empty. We also talked about how time-consuming they were, and we gave a real-world scenario to show how they may be used. You may improve your programming abilities and efficiently manage data in a variety of situations by knowing linked lists.