Facebook Pixel

82. Remove Duplicates from Sorted List II

Problem Description

You are given the head of a sorted linked list that may contain duplicate values. Your task is to delete all nodes that have duplicate numbers, removing both the duplicates and the original occurrence. Only nodes with distinct (unique) values should remain in the final linked list.

The resulting linked list should still be sorted.

For example:

  • If the input linked list is 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5, the output should be 1 -> 2 -> 5 (both 3 and 4 appear multiple times, so all occurrences are removed)
  • If the input linked list is 1 -> 1 -> 1 -> 2 -> 3, the output should be 2 -> 3 (the value 1 appears multiple times, so all occurrences are removed)

The key point is that if a value appears more than once in the linked list, you need to remove all nodes with that value, not just the duplicates.

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

Intuition

Since we need to remove all nodes with duplicate values (including the original), we face a challenge: how do we know if a node should be kept or removed? We need to look ahead to see if the current node's value appears again.

The key insight is to use two pointers: one (pre) that stays at the last valid node we want to keep, and another (cur) that explores ahead to find duplicates.

When we encounter a node, we need to check if it has duplicates by looking at subsequent nodes. If cur.val == cur.next.val, we know this value appears multiple times, so we need to skip all nodes with this value. We keep moving cur forward as long as we see the same value.

After skipping all duplicates of a value, we need to determine if any duplicates were actually found. This is where the condition pre.next == cur becomes crucial:

  • If pre.next == cur, it means cur didn't move forward (no duplicates were found), so this node should be kept. We move pre to cur.
  • If pre.next != cur, it means cur moved forward (duplicates were found), so we need to skip all these duplicate nodes by setting pre.next = cur.next.

We use a dummy head node to handle the edge case where the first few nodes of the original list need to be removed. Without the dummy node, we'd have to write special logic to update the head pointer when removing nodes from the beginning.

This approach allows us to remove all duplicate nodes in a single pass through the linked list, maintaining the sorted order naturally since we're only removing nodes, not rearranging them.

Learn more about Linked List and Two Pointers patterns.

Solution Approach

We implement a single-pass solution using two pointers to track and remove all nodes with duplicate values.

Step 1: Setup dummy node and pointers

  • Create a dummy head node dummy and set dummy.next = head to handle edge cases where the first nodes need to be removed
  • Initialize pointer pre pointing to dummy (tracks the last valid node to keep)
  • Initialize pointer cur pointing to head (explores the list to find duplicates)

Step 2: Traverse the linked list While cur is not null:

  1. Find all duplicates of current value

    • While cur.next exists and cur.next.val == cur.val:
      • Move cur forward: cur = cur.next
    • This loop positions cur at the last node of a sequence of duplicate values
  2. Determine if duplicates were found

    • Check if pre.next == cur:
      • If true: No duplicates were found (cur didn't move), so this node should be kept
        • Move pre forward: pre = cur
      • If false: Duplicates were found (cur moved forward), so skip all these nodes
        • Connect pre to the node after all duplicates: pre.next = cur.next
  3. Move to next node

    • Move cur forward: cur = cur.next

Step 3: Return result

  • Return dummy.next as the new head of the modified linked list

Example walkthrough: For linked list 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5:

  • Start: pre at dummy, cur at 1
  • Node 1: No duplicates, pre moves to 1
  • Node 2: No duplicates, pre moves to 2
  • Node 3: Found duplicate (next is also 3), cur moves to second 3, then pre.next = cur.next (connects 2 to 4)
  • Node 4: Found duplicate (next is also 4), cur moves to second 4, then pre.next = cur.next (connects 2 to 5)
  • Node 5: No duplicates, pre moves to 5
  • Result: 1 -> 2 -> 5

The time complexity is O(n) where n is the number of nodes, and space complexity is O(1) as we only use a constant amount of extra space.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through the solution with the linked list: 1 -> 1 -> 2 -> 3 -> 3

Initial Setup:

  • Create dummy node pointing to head: dummy -> 1 -> 1 -> 2 -> 3 -> 3
  • pre points to dummy
  • cur points to first 1

Iteration 1: (cur at first 1)

  • Check for duplicates: cur.next.val (1) == cur.val (1)
  • Move cur forward to second 1
  • Now cur is at the last 1 in the sequence
  • Check if duplicates found: pre.next (first 1) != cur (second 1) → duplicates found!
  • Skip all 1s: pre.next = cur.next (dummy now points to 2)
  • Move cur to 2
  • State: dummy -> 2 -> 3 -> 3, pre at dummy, cur at 2

Iteration 2: (cur at 2)

  • Check for duplicates: cur.next.val (3) != cur.val (2)
  • No duplicates found: pre.next (2) == cur (2)
  • Keep this node: pre = cur (pre moves to 2)
  • Move cur to first 3
  • State: dummy -> 2 -> 3 -> 3, pre at 2, cur at first 3

Iteration 3: (cur at first 3)

  • Check for duplicates: cur.next.val (3) == cur.val (3)
  • Move cur forward to second 3
  • Now cur is at the last 3 in the sequence
  • Check if duplicates found: pre.next (first 3) != cur (second 3) → duplicates found!
  • Skip all 3s: pre.next = cur.next (2 now points to null)
  • Move cur to null
  • State: dummy -> 2, pre at 2, cur at null

Final Result: Return dummy.next which gives us: 2

The linked list 1 -> 1 -> 2 -> 3 -> 3 becomes just 2 after removing all nodes with duplicate values.

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
7from typing import Optional
8
9class Solution:
10    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
11        # Create a dummy node pointing to head to handle edge cases
12        dummy = ListNode(next=head)
13      
14        # Previous pointer starts at dummy node
15        prev = dummy
16      
17        # Current pointer starts at head
18        curr = head
19      
20        # Iterate through the linked list
21        while curr:
22            # Skip all nodes with the same value as current node
23            while curr.next and curr.next.val == curr.val:
24                curr = curr.next
25          
26            # If prev.next still points to curr, no duplicates were found
27            if prev.next == curr:
28                # Move prev pointer forward
29                prev = curr
30            else:
31                # Duplicates were found, skip all duplicate nodes
32                prev.next = curr.next
33          
34            # Move current pointer to the next node
35            curr = curr.next
36      
37        # Return the head of the modified list
38        return dummy.next
39
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 deleteDuplicates(ListNode head) {
13        // Create a dummy node pointing to head to handle edge cases
14        ListNode dummy = new ListNode(0, head);
15      
16        // Previous node pointer to maintain the list after removing duplicates
17        ListNode previous = dummy;
18      
19        // Current node pointer to traverse the list
20        ListNode current = head;
21      
22        // Traverse the entire linked list
23        while (current != null) {
24            // Skip all nodes with the same value as current node
25            while (current.next != null && current.next.val == current.val) {
26                current = current.next;
27            }
28          
29            // Check if we found duplicates
30            if (previous.next == current) {
31                // No duplicates found, move previous pointer forward
32                previous = current;
33            } else {
34                // Duplicates found, skip all duplicate nodes
35                previous.next = current.next;
36            }
37          
38            // Move to the next node
39            current = current.next;
40        }
41      
42        // Return the head of the modified list
43        return dummy.next;
44    }
45}
46
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 *     int val;
5 *     ListNode *next;
6 *     ListNode() : val(0), next(nullptr) {}
7 *     ListNode(int x) : val(x), next(nullptr) {}
8 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13    ListNode* deleteDuplicates(ListNode* head) {
14        // Create a dummy node pointing to head to handle edge cases
15        ListNode* dummyNode = new ListNode(0, head);
16      
17        // Previous pointer tracks the last node before potential duplicates
18        ListNode* previous = dummyNode;
19      
20        // Current pointer traverses the list
21        ListNode* current = head;
22      
23        // Iterate through the entire linked list
24        while (current != nullptr) {
25            // Skip all nodes with the same value as current
26            while (current->next != nullptr && current->next->val == current->val) {
27                current = current->next;
28            }
29          
30            // Check if we found duplicates
31            if (previous->next == current) {
32                // No duplicates found, move previous pointer forward
33                previous = current;
34            } else {
35                // Duplicates found, skip all duplicate nodes
36                previous->next = current->next;
37            }
38          
39            // Move to the next node
40            current = current->next;
41        }
42      
43        // Return the head of the modified list
44        return dummyNode->next;
45    }
46};
47
1/**
2 * Definition for singly-linked list.
3 * class ListNode {
4 *     val: number
5 *     next: ListNode | null
6 *     constructor(val?: number, next?: ListNode | null) {
7 *         this.val = (val===undefined ? 0 : val)
8 *         this.next = (next===undefined ? null : next)
9 *     }
10 * }
11 */
12
13/**
14 * Removes all nodes from a sorted linked list that have duplicate values.
15 * Only nodes with unique values are kept in the resulting list.
16 * 
17 * @param head - The head of the sorted linked list
18 * @returns The head of the modified linked list with duplicates removed
19 */
20function deleteDuplicates(head: ListNode | null): ListNode | null {
21    // Create a dummy node pointing to the head to handle edge cases
22    const dummyNode: ListNode = new ListNode(0, head);
23  
24    // Previous node pointer - starts at dummy to handle deletion of head
25    let previousNode: ListNode = dummyNode;
26  
27    // Current node pointer for traversal
28    let currentNode: ListNode | null = head;
29  
30    // Traverse the entire linked list
31    while (currentNode) {
32        // Skip all consecutive nodes with the same value as current
33        while (currentNode.next && currentNode.val === currentNode.next.val) {
34            currentNode = currentNode.next;
35        }
36      
37        // Check if we found duplicates
38        if (previousNode.next === currentNode) {
39            // No duplicates found, move previous pointer forward
40            previousNode = currentNode;
41        } else {
42            // Duplicates found, skip all nodes with duplicate values
43            previousNode.next = currentNode.next;
44        }
45      
46        // Move to the next node
47        currentNode = currentNode.next;
48    }
49  
50    // Return the head of the modified list
51    return dummyNode.next;
52}
53

Time and Space Complexity

Time Complexity: O(n), where n is the length of the linked list.

The algorithm uses a single pass through the linked list. The outer while loop iterates through each unique position in the list, and the inner while loop skips over duplicate nodes. Even though there are nested loops, each node is visited at most twice (once by the outer loop's cur pointer and potentially once by the inner loop when checking for duplicates). Therefore, the total number of operations is proportional to the number of nodes in the list, resulting in linear time complexity.

Space Complexity: O(1)

The algorithm only uses a constant amount of extra space. It creates a few pointer variables (dummy, pre, and cur) to traverse and modify the linked list in-place. No additional data structures that grow with the input size are used. The space usage remains constant regardless of the length of the input linked list.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrectly handling consecutive groups of duplicates

The Problem: A common mistake is not properly resetting the pointers when dealing with consecutive groups of duplicates. For example, with input 1 -> 2 -> 2 -> 3 -> 3 -> 4, after removing the 2s, if the logic doesn't correctly position the pointers, it might fail to detect the 3s as duplicates.

Why it happens: After removing a group of duplicates by setting prev.next = curr.next, the curr pointer moves to curr.next. If we immediately move prev forward without checking if this new position also contains duplicates, we'll miss them.

The Solution: The provided code correctly handles this by NOT moving prev forward when duplicates are found. The key is the conditional check:

if prev.next == curr:
    prev = curr  # Only move prev if no duplicates were found
else:
    prev.next = curr.next  # Skip duplicates, but keep prev in place

This ensures prev stays at the last valid node until we find a non-duplicate value.

Pitfall 2: Forgetting to use a dummy node

The Problem: Without a dummy node, handling cases where the first element(s) need to be removed becomes complex. For input like 1 -> 1 -> 2 -> 3, we need to remove all 1s and return 2 as the new head.

Why it happens: Direct manipulation of the head pointer requires special case handling:

  • Need to track when the head itself changes
  • Must update the head reference multiple times if initial elements are duplicates
  • Edge case when all elements are duplicates (should return None)

The Solution: Always use a dummy node that points to the original head:

dummy = ListNode(next=head)
prev = dummy

This provides a stable reference point and eliminates special cases. The actual head of the result is always dummy.next, regardless of what nodes were removed.

Pitfall 3: Off-by-one error in duplicate detection

The Problem: Using the wrong comparison logic like checking curr.val == prev.val instead of curr.next.val == curr.val can lead to incorrect duplicate detection.

Why it happens: Confusion about which nodes to compare - should we look backward or forward? Looking backward would require keeping track of previous values and makes the logic more complex.

The Solution: Always look forward to detect duplicates:

while curr.next and curr.next.val == curr.val:
    curr = curr.next

This approach naturally groups all nodes with the same value together and positions curr at the last node of any duplicate sequence, making it easy to skip the entire group if needed.

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

Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?


Recommended Readings

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

Load More