Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Singly Linked List: Insertion & Deletion

Recommended resources

Hello! Welcome to your third lesson on data structures.

In our last session, we built the basic structure of a singly linked list using Node and LinkedList classes and implemented the fundamental traversal algorithm. You learned that while traversing a linked list takes time, it's the gateway to all other operations.

Today, we'll implement the very operations that give linked lists their power: insertion and deletion. Your learning outcome is to implement insertion and deletion operations in a singly linked list (head, tail, and by value). Mastering these pointer manipulations is non-negotiable for technical interviews, as they form the basis for solving many complex list-based problems. We'll pay close attention to the logic, edge cases, and performance implications—all critical aspects for an interview setting.

Let's expand the LinkedList class we started in the previous lesson.

# Refresher from last lesson
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

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

    def print_list(self):
        current = self.head
        values = []
        while current:
            values.append(str(current.value))
            current = current.next
        print(" -> ".join(values) if values else "List is empty")

# We will be adding methods to this LinkedList class

1. Insertion Operations

Insertion in a linked list is all about "rewiring" the next pointers. We create a new node and then adjust the pointers of the surrounding nodes to link it into the chain.

For a clear visual and animated explanation of the different insertion types, the following video is an excellent resource.

Singly Linked List | Insert, Delete, Complexity Analysis

Watch the 'Singly Linked List | Insert, Delete, Complexity Analysis' video from Blue Tree Code to see animated examples of insertion.

Please watch from 01:41 to 07:46. This segment covers adding a node to the end, the start, and after a given node. Focus on how the next pointers are manipulated in each case. The code is in Java, but the logic and animations are perfectly transferable to Python.

Insertion at the Head (Prepending)

This is the most efficient insertion operation. It involves just two steps:

  1. Set the next pointer of the new node to point to the current head of the list.
  2. Update the list's head to be the new node.

This process is completed in constant time, , because it doesn't depend on the length of the list.

Insertion at the Beginning of Singly Linked List
This diagram shows the two pointer updates required to insert a new node 'E' at the beginning of a list. The new node's `next` pointer is set to the old head 'A', and the list's `head` pointer is updated to point to 'E'.

Here's the implementation:

# Add this method to your LinkedList class
def insert_at_beginning(self, value):
    new_node = Node(value)
    new_node.next = self.head
    self.head = new_node

Insertion at the Tail (Appending)

This is a very common operation. To add a node at the end, we must first find the current last node.

  1. Edge Case: If the list is empty (self.head is None), the new node simply becomes the head.
  2. General Case: Traverse the list until you find the node whose next pointer is None. This is the last node.
  3. Update the next pointer of this last node to point to the new node.
Insertion at the End of a Singly Linked List
To append node 'E', we first traverse to the last node 'D'. Then, we update D's `next` pointer from NULL to point to 'E'.

This operation has a time complexity of because, in the worst case, you have to traverse the entire list to find the end.

Here's the implementation:

# Add this method to your LinkedList class
def insert_at_end(self, value):
    new_node = Node(value)
    if self.head is None:
        self.head = new_node
        return

    # Traverse to the last node
    last_node = self.head
    while last_node.next:
        last_node = last_node.next
    
    last_node.next = new_node

Insertion by Value or Position

Inserting a node in the middle of the list (e.g., after a node with a specific value, or at a specific index) also requires traversal. The key is to stop at the node before the desired insertion point.

Let's implement insert_at_position(self, value, position).

  1. Handle edge cases: an invalid position or position == 0 (which is just insert_at_beginning).
  2. Traverse the list to find the node at position - 1.
  3. "Splice" the new node into the list by updating pointers:
    • Set the new node's next pointer to the next of the node at position - 1.
    • Set the next pointer of the node at position - 1 to the new node.

Like appending, this is an operation due to the traversal.

Mastering Linked List Problems for Interviews

For concise, interview-ready Python code for these insertion operations, refer to the article from Algocademy.

Review the section named '2. Basic Linked List Operations' and find the subsection 'Insertion'. Study the Python functions insert_at_beginning, insert_at_end, and insert_at_position. Compare them with the methods we've just discussed.


2. Deletion Operations

Deletion also involves "rewiring" pointers, but this time to "bypass" the node being removed. A crucial detail is that to delete a node, you need a reference to the node preceding it.

Deletion from the Head

This is the counterpart to inserting at the head. It's simple and efficient.

  1. Edge Case: If the list is empty, there's nothing to do.
  2. General Case: Update the head pointer to self.head.next. The old head is now "orphaned," and in a garbage-collected language like Python, its memory will be reclaimed automatically.

This is an operation.

# Add this method to your LinkedList class
def delete_from_beginning(self):
    if not self.head:
        print("List is empty. Cannot delete.")
        return
    self.head = self.head.next

Deletion by Value

This is a common interview task. To delete a node with a specific value, we must find it while keeping track of its predecessor.

  1. Edge Case: If the node to be deleted is the head, handle it separately (same as delete_from_beginning).
  2. General Case: Use a current pointer to traverse the list. When current.next is the node to be deleted, you've found the predecessor.
  3. Bypass the target node by setting current.next = current.next.next.

This operation is due to the search.

# Add this method to your LinkedList class
def delete_by_value(self, value):
    if not self.head:
        return

    # If the head node itself holds the value to be deleted
    if self.head.value == value:
        self.head = self.head.next
        return

    current = self.head
    # Search for the node to be deleted, keep track of the previous node
    while current.next and current.next.value != value:
        current = current.next

    # If the value was found, current is the node *before* the one to delete
    if current.next:
        current.next = current.next.next
    else:
        # Value not found in the list
        print(f"Value {value} not found.")

Deletion from the Tail

This is surprisingly tricky in a singly linked list. To delete the last node, you must set the next pointer of the second-to-last node to None. This means you have to traverse the list to find that second-to-last node.

  1. Edge Cases: Handle an empty list or a list with only one node.
  2. General Case: Traverse until current.next.next is None. At this point, current is the second-to-last node.
  3. Set current.next = None.

This is also an operation.

Linked List Interview Questions & Tips for Senior Engineers

The article 'Linked List Interview Questions & Tips' provides a good textual summary of the deletion process and discusses the complexities involved.

Read the section titled 'Deletion from a Singly Linked List', including the subsections on Head, Middle, and Tail deletion. Note the discussion on time and space complexity.

Test your understanding!

Given the LinkedList class we've been building, why is deleting the last node while deleting the first node is ? How could you change the LinkedList class structure to make deleting the last node an operation?

Show answer

Deleting the last node is because, in a singly linked list, you can only move forward. To remove the last node, you need access to the second-to-last node to set its next pointer to None. Finding this node requires traversing the entire list from the head.

To make this an operation, you would need to use a doubly linked list, where each node has both a next and a prev pointer. Alternatively, for a singly linked list, you could maintain a tail pointer in your LinkedList class that always points to the last node. However, even with a tail pointer, deletion remains because you would still need to traverse the list to find the new tail (the second-to-last node). A doubly linked list is the true solution for tail deletion.


An Interview Pro-Tip: The Dummy Node

When implementing list manipulations, especially deletion, you often have to write special if conditions to handle the head node. A common technique to simplify this logic is to use a dummy node (or sentinel node).

A dummy node is a placeholder node that sits before the actual head of your list. Your operations then start from dummy, and the real list begins at dummy.next. This way, every node you want to delete, including the original head, has a preceding node, eliminating the need for special case handling.

Linked List Interview Questions & Tips for Senior Engineers

The article from interviewing.io also introduces this powerful technique.

Read the section 'How to Show Mastery of Linked Lists in Interviews', focusing on 'Using a Dummy Node'. Compare the code for deletion with and without the dummy node to see how it simplifies the logic. This is a sign of a seasoned candidate in an interview.


Conclusion

You've now implemented the complete set of fundamental CRUD (Create, Read, Update, Delete) operations for a singly linked list.

Key Takeaways:

  • Insertion and Deletion are about manipulating next pointers to either splice a node in or bypass it.
  • Head Operations (insertion/deletion at the beginning) are highly efficient at .
  • Tail and Mid-list Operations are less efficient at because they require traversal from the head.
  • Edge Cases are Critical: Always consider the empty list, a single-node list, and operations on the head/tail. Using a dummy node is a pro-level technique to simplify head-related edge cases.

In our next lesson, we will build directly on these pointer manipulation skills to learn high-ROI interview patterns. We'll start with the Two-Pointer Technique, a versatile method used to solve a wide range of array and linked list problems, such as finding the middle of a list or detecting cycles.