Facebook Pixel

147. Insertion Sort List

Problem Description

You are given the head of a singly linked list. Your task is to sort this linked list using the insertion sort algorithm and return the head of the sorted list.

How Insertion Sort Works:

Insertion sort builds the final sorted list one element at a time. It works similarly to how you might sort playing cards in your hands:

  1. Start with an empty sorted portion (or consider the first element as already sorted)
  2. Take one element from the unsorted portion
  3. Find the correct position in the sorted portion where this element should be inserted
  4. Insert the element at that position
  5. Repeat until all elements are processed

For a Linked List:

  • Initially, consider the first node as the sorted portion
  • For each subsequent node:
    • Remove it from its current position
    • Traverse the sorted portion from the beginning to find where it belongs
    • Insert it at the correct position to maintain sorted order
    • Continue with the next unsorted node

Example: If the input linked list is 4 -> 2 -> 1 -> 3, the sorting process would be:

  • Start: 4 (sorted) | 2 -> 1 -> 3 (unsorted)
  • Step 1: Insert 2 → 2 -> 4 (sorted) | 1 -> 3 (unsorted)
  • Step 2: Insert 1 → 1 -> 2 -> 4 (sorted) | 3 (unsorted)
  • Step 3: Insert 3 → 1 -> 2 -> 3 -> 4 (sorted)

The algorithm maintains the linked list structure while rearranging the nodes to achieve sorted order based on their values in ascending order.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When implementing insertion sort on a linked list, we need to think about how to efficiently manage node connections while sorting. Unlike arrays where we shift elements, in linked lists we manipulate pointers.

The key insight is to use a dummy node at the beginning of our sorted portion. This dummy node serves as an anchor point that simplifies our insertion logic - we never have to worry about updating the head of the list separately.

Here's the thought process:

  1. Why check if pre.val <= cur.val? If the current node is already in the correct position (greater than or equal to the previous node), we don't need to move it. This optimization saves us from unnecessary traversal and pointer manipulation.

  2. Finding the insertion point: When we need to move a node, we start from the dummy node and traverse the sorted portion until we find where cur belongs. We're looking for the spot where p.next.val > cur.val - this means cur should be inserted between p and p.next.

  3. The pointer manipulation dance:

    • Save cur.next in a temporary variable t (we'll need this to continue iteration)
    • Insert cur into its new position by setting cur.next = p.next and p.next = cur
    • Fix the gap left by removing cur by setting pre.next = t
    • Move to the next unsorted node by setting cur = t

The beauty of this approach is that we're building our sorted list in-place. We're not creating new nodes - just rearranging the existing ones. The dummy node eliminates edge cases when inserting at the beginning, and by maintaining a pre pointer, we can efficiently remove nodes from their current position without losing track of where we are in the traversal.

Solution Approach

Let's walk through the implementation step by step:

Initial Setup:

if head is None or head.next is None:
    return head

First, we handle edge cases - if the list is empty or has only one node, it's already sorted.

dummy = ListNode(head.val, head)
pre, cur = dummy, head

We create a dummy node that points to the head. This dummy acts as a sentinel before our sorted portion. We initialize two pointers:

  • pre: tracks the last node in the sorted portion
  • cur: points to the current node being processed

Main Sorting Loop:

while cur:

We iterate through each node in the original list.

Optimization Check:

if pre.val <= cur.val:
    pre, cur = cur, cur.next
    continue

If the current node is already in the correct position (greater than or equal to the previous node), we simply advance both pointers. This is an important optimization that keeps already-sorted sequences intact.

Finding Insertion Position:

p = dummy
while p.next.val <= cur.val:
    p = p.next

When we need to move a node, we start from the dummy node and traverse the sorted portion. We stop when p.next.val > cur.val, meaning cur should be inserted between p and p.next.

Pointer Manipulation:

t = cur.next          # Save the next node to process
cur.next = p.next     # Insert cur after p
p.next = cur          # Complete the insertion
pre.next = t          # Remove cur from its old position
cur = t               # Move to the next unsorted node

This sequence carefully:

  1. Saves the reference to the next unsorted node (t)
  2. Inserts cur into its correct position in the sorted portion
  3. Patches the gap left by removing cur from its original position
  4. Moves to process the next node

Return Result:

return dummy.next

Since our dummy node has the same value as the original head, dummy.next points to the actual head of our sorted list.

Time Complexity: O(n²) - In the worst case (reverse sorted list), for each of the n nodes, we might traverse up to n nodes to find the insertion position.

Space Complexity: O(1) - We only use a constant amount of extra space for pointers and the dummy node.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through sorting the linked list 3 -> 1 -> 4 -> 2 step by step:

Initial State:

  • Create dummy node with value 3, pointing to head
  • dummy -> 3 -> 1 -> 4 -> 2
  • pre = dummy, cur = 3 (first node)

Iteration 1: Process node 3

  • Check: Is pre.val (3) <= cur.val (3)? Yes!
  • No movement needed, advance pointers
  • pre = 3, cur = 1
  • List unchanged: 3 -> 1 -> 4 -> 2

Iteration 2: Process node 1

  • Check: Is pre.val (3) <= cur.val (1)? No, need to move 1
  • Find insertion position: Start at dummy, traverse while p.next.val <= 1
    • p = dummy, check p.next.val (3) <= 1? No, stop here
    • Insert 1 after dummy (before 3)
  • Pointer manipulation:
    • t = 4 (save next node)
    • 1.next = 3 (insert 1 before 3)
    • dummy.next = 1 (dummy points to 1)
    • 3.next = 4 (fix the gap)
    • cur = 4 (move to next)
  • List becomes: 1 -> 3 -> 4 -> 2

Iteration 3: Process node 4

  • Check: Is pre.val (3) <= cur.val (4)? Yes!
  • No movement needed, advance pointers
  • pre = 4, cur = 2
  • List unchanged: 1 -> 3 -> 4 -> 2

Iteration 4: Process node 2

  • Check: Is pre.val (4) <= cur.val (2)? No, need to move 2
  • Find insertion position: Start at dummy, traverse while p.next.val <= 2
    • p = dummy, check p.next.val (1) <= 2? Yes, continue
    • p = 1, check p.next.val (3) <= 2? No, stop here
    • Insert 2 between 1 and 3
  • Pointer manipulation:
    • t = None (no next node)
    • 2.next = 3 (insert 2 before 3)
    • 1.next = 2 (1 points to 2)
    • 4.next = None (fix the gap)
    • cur = None (done)
  • List becomes: 1 -> 2 -> 3 -> 4

Final Result: Return dummy.next which points to 1, giving us the sorted list 1 -> 2 -> 3 -> 4

Solution Implementation

1# Definition for singly-linked list.
2# class ListNode:
3#     def __init__(self, val=0, next=None):
4#         self.val = val
5#         self.next = next
6
7class Solution:
8    def insertionSortList(self, head: ListNode) -> ListNode:
9        # Handle edge cases: empty list or single node
10        if head is None or head.next is None:
11            return head
12      
13        # Create a dummy node to simplify insertion at the beginning
14        # Initialize with minimum possible value to avoid comparison issues
15        dummy = ListNode(float('-inf'))
16        dummy.next = head
17      
18        # prev_node: last node in the sorted portion
19        # curr_node: current node being processed
20        prev_node = head
21        curr_node = head.next
22      
23        while curr_node:
24            # If current node is already in correct position (greater than or equal to previous)
25            if prev_node.val <= curr_node.val:
26                # Move to next node
27                prev_node = curr_node
28                curr_node = curr_node.next
29                continue
30          
31            # Find the correct position to insert curr_node in the sorted portion
32            insert_pos = dummy
33            while insert_pos.next.val < curr_node.val:
34                insert_pos = insert_pos.next
35          
36            # Remove curr_node from its current position
37            next_to_process = curr_node.next
38            prev_node.next = next_to_process
39          
40            # Insert curr_node at the found position
41            curr_node.next = insert_pos.next
42            insert_pos.next = curr_node
43          
44            # Move to the next node to process
45            curr_node = next_to_process
46      
47        # Return the head of the sorted list (skip dummy node)
48        return dummy.next
49
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 *     int val;
5 *     ListNode next;
6 *     ListNode() {}
7 *     ListNode(int val) { this.val = val; }
8 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12    public ListNode insertionSortList(ListNode head) {
13        // Handle edge cases: empty list or single node
14        if (head == null || head.next == null) {
15            return head;
16        }
17      
18        // Create a dummy node with value smaller than any possible value
19        // This serves as the head of our sorted portion
20        ListNode dummy = new ListNode(Integer.MIN_VALUE);
21        dummy.next = head;
22      
23        // Previous node in the original list and current node to be inserted
24        ListNode previousNode = head;
25        ListNode currentNode = head.next;
26      
27        // Process each node in the original list
28        while (currentNode != null) {
29            // If current node is already in correct position (greater than or equal to previous)
30            // Simply move forward
31            if (previousNode.val <= currentNode.val) {
32                previousNode = currentNode;
33                currentNode = currentNode.next;
34                continue;
35            }
36          
37            // Find the correct insertion position in the sorted portion
38            ListNode insertPosition = dummy;
39            while (insertPosition.next.val < currentNode.val) {
40                insertPosition = insertPosition.next;
41            }
42          
43            // Remove current node from its current position
44            ListNode nextNode = currentNode.next;
45            previousNode.next = nextNode;
46          
47            // Insert current node at the found position
48            currentNode.next = insertPosition.next;
49            insertPosition.next = currentNode;
50          
51            // Move to the next node to be processed
52            currentNode = nextNode;
53        }
54      
55        // Return the head of the sorted list (skip dummy node)
56        return dummy.next;
57    }
58}
59
1/**
2 * Definition for singly-linked list.
3 */
4struct ListNode {
5    int val;
6    ListNode* next;
7    ListNode() : val(0), next(nullptr) {}
8    ListNode(int x) : val(x), next(nullptr) {}
9    ListNode(int x, ListNode* next) : val(x), next(next) {}
10};
11
12class Solution {
13public:
14    /**
15     * Sorts a linked list using insertion sort algorithm
16     * @param head - The head of the linked list to be sorted
17     * @return The head of the sorted linked list
18     */
19    ListNode* insertionSortList(ListNode* head) {
20        // Handle edge cases: empty list or single node
21        if (head == nullptr || head->next == nullptr) {
22            return head;
23        }
24      
25        // Create a dummy node to simplify insertion at the beginning
26        // Initialize dummy with a minimal value and point to nullptr initially
27        ListNode* dummy = new ListNode(INT_MIN);
28      
29        // Current node being processed from the original list
30        ListNode* current = head;
31      
32        // Process each node in the original list
33        while (current != nullptr) {
34            // Store the next node to process before modifying pointers
35            ListNode* next_node = current->next;
36          
37            // Find the correct position to insert current node in sorted portion
38            ListNode* insert_position = dummy;
39            while (insert_position->next != nullptr && 
40                   insert_position->next->val < current->val) {
41                insert_position = insert_position->next;
42            }
43          
44            // Insert current node at the found position
45            current->next = insert_position->next;
46            insert_position->next = current;
47          
48            // Move to the next node in the original list
49            current = next_node;
50        }
51      
52        // Store the head of sorted list and clean up dummy node
53        ListNode* sorted_head = dummy->next;
54        delete dummy;
55      
56        // Return the sorted list
57        return sorted_head;
58    }
59};
60
1/**
2 * Definition for singly-linked list node
3 */
4interface ListNode {
5    val: number;
6    next: ListNode | null;
7}
8
9/**
10 * Sorts a linked list using insertion sort algorithm
11 * @param head - The head of the linked list to be sorted
12 * @returns The head of the sorted linked list
13 */
14function insertionSortList(head: ListNode | null): ListNode | null {
15    // Handle edge cases: empty list or single node
16    if (head === null || head.next === null) {
17        return head;
18    }
19  
20    // Create a dummy node to simplify insertion at the beginning
21    // Initialize dummy with head's value and point to head
22    const dummy: ListNode = {
23        val: head.val,
24        next: head
25    };
26  
27    // Previous node in the original list and current node being processed
28    let previous: ListNode = dummy;
29    let current: ListNode | null = head;
30  
31    // Process each node in the original list
32    while (current !== null) {
33        // If current node is already in correct position (greater than or equal to previous)
34        if (previous.val <= current.val) {
35            // Move forward without any insertion
36            previous = current;
37            current = current.next;
38            continue;
39        }
40      
41        // Find the correct position to insert current node
42        let insertPosition: ListNode = dummy;
43        while (insertPosition.next !== null && insertPosition.next.val <= current.val) {
44            insertPosition = insertPosition.next;
45        }
46      
47        // Store the next node to process
48        const nextNode: ListNode | null = current.next;
49      
50        // Insert current node at the found position
51        current.next = insertPosition.next;
52        insertPosition.next = current;
53      
54        // Connect previous node to the next node (skip current)
55        previous.next = nextNode;
56      
57        // Move to the next node
58        current = nextNode;
59    }
60  
61    // Return the sorted list (skip dummy node)
62    return dummy.next;
63}
64

Time and Space Complexity

Time Complexity: O(n²)

The algorithm implements insertion sort on a linked list. In the worst case (when the list is sorted in reverse order), for each node we need to:

  • Traverse from the dummy node to find the correct insertion position
  • For the i-th node, we may need to traverse up to i nodes to find the insertion point
  • This results in 1 + 2 + 3 + ... + n = n(n+1)/2 operations
  • Therefore, the time complexity is O(n²)

In the best case (when the list is already sorted), the condition pre.val <= cur.val is always true, so we simply traverse the list once, giving O(n) time complexity. However, we consider the worst-case for Big-O notation.

Space Complexity: O(1)

The algorithm only uses a constant amount of extra space:

  • A dummy node created at the beginning
  • A few pointer variables (pre, cur, p, t)
  • No additional data structures that scale with input size
  • The sorting is done in-place by rearranging the existing nodes

Therefore, the space complexity is O(1) (constant space).

Common Pitfalls

Pitfall 1: Incorrect Dummy Node Initialization

Problem: The original code uses ListNode(head.val, head) to create the dummy node. This creates a duplicate node with the same value as the head, which becomes part of the final list and causes incorrect results.

Why it happens: The constructor ListNode(head.val, head) creates a new node with head.val as its value and head as its next pointer. This means you're adding an extra node to your list rather than just creating a sentinel.

Correct approach:

# Wrong
dummy = ListNode(head.val, head)

# Correct
dummy = ListNode(float('-inf'))  # or any value less than all possible node values
dummy.next = head

Pitfall 2: Not Handling the Boundary Between Sorted and Unsorted Portions

Problem: When moving a node from the unsorted portion to the sorted portion, failing to properly update the prev pointer can lead to losing track of where the sorted portion ends.

Example scenario: If you insert a node into the sorted portion but don't maintain prev correctly, you might process the same nodes multiple times or skip nodes.

Solution: After inserting a node, prev should remain pointing to the last node that was naturally in order (not moved). Don't update prev when you move a node backward.

Pitfall 3: Infinite Loop Due to Incorrect Pointer Updates

Problem: If the pointer manipulation sequence is wrong, you might create cycles in the linked list or lose references to parts of the list.

Common mistake:

# Wrong order - creates potential issues
cur.next = p.next     
pre.next = cur.next   # This now points to p.next, not the original cur.next!
p.next = cur

Correct sequence:

# Save next node first
next_node = cur.next
# Remove cur from current position
pre.next = next_node
# Insert cur in new position
cur.next = p.next
p.next = cur
# Update cur for next iteration
cur = next_node

Pitfall 4: Comparison Logic Error in Finding Insert Position

Problem: Using <= instead of < when finding the insertion position can cause duplicate values to be placed in the wrong order or create an infinite loop.

Why it matters: For stable sorting (maintaining relative order of equal elements), you should insert equal elements after existing equal elements, not before them.

# Potentially problematic for duplicates
while p.next.val <= cur.val:  # might skip past equal values
    p = p.next

# Better for stability
while p.next and p.next.val < cur.val:  # stops at first equal or greater value
    p = p.next

Pitfall 5: Forgetting to Check for Null Before Accessing Values

Problem: When traversing to find the insertion position, forgetting to check if p.next exists before accessing p.next.val causes null pointer exceptions.

Fix:

# Add null check
while p.next and p.next.val < cur.val:
    p = p.next

These pitfalls are particularly tricky because they might work for some test cases but fail on edge cases like lists with duplicate values, already sorted lists, or single-element lists.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More