Facebook Pixel

1836. Remove Duplicates From an Unsorted Linked List 🔒

MediumHash TableLinked List
Leetcode Link

Problem Description

You are given the head of a linked list that contains integer values. Your task is to identify all values that appear more than once in the linked list and completely remove all nodes containing those duplicate values.

The key points to understand:

  • If a value appears multiple times in the linked list, you need to delete all nodes with that value, not just the duplicates
  • The linked list is unsorted, meaning duplicate values can appear anywhere in the list
  • You need to return the modified linked list after removing all nodes with duplicate values

For example, if you have a linked list like 1 -> 2 -> 3 -> 2 -> 4, the value 2 appears twice. You would need to remove both nodes with value 2, resulting in 1 -> 3 -> 4.

The solution uses a two-pass approach:

  1. First pass: Count the frequency of each value using a hash table (Counter). This traverses the entire linked list to identify which values appear more than once.
  2. Second pass: Traverse the linked list again, this time removing any node whose value has a count greater than 1 in the hash table.

The code uses a dummy node technique to simplify edge cases (like when the head itself needs to be deleted). It maintains two pointers:

  • pre: Points to the node before the current node (used to skip/delete nodes)
  • cur: Points to the current node being examined

When a node needs to be deleted (its value count > 1), the code updates pre.next = cur.next to skip over the current node. When a node is kept, pre advances to the current node.

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

Intuition

The challenge here is that we need to remove all occurrences of values that appear more than once, not just the duplicates. This means we can't simply remove nodes as we encounter duplicates during a single traversal - we need to know upfront which values are duplicated throughout the entire list.

Think about it this way: if we're at a node with value 5, we can't decide whether to keep or delete it without knowing if another 5 exists somewhere else in the list. This naturally leads us to a two-pass strategy.

The first insight is that we need complete information about value frequencies before making any deletion decisions. A hash table is perfect for this - we can traverse the list once and count how many times each value appears. This gives us a "blueprint" of which values need to be eliminated.

The second insight involves the deletion process itself. When traversing a linked list and potentially deleting nodes, we need to maintain connections properly. If we delete a node, we need the previous node to point to the next node, effectively "skipping over" the deleted node. This is why we maintain two pointers: pre (previous) and cur (current).

A clever trick used here is the dummy node. Since we might need to delete the head of the list, having a dummy node before the head simplifies our logic - we don't need special handling for the head deletion case. The dummy node ensures we always have a pre node to work with.

The beauty of this approach is its simplicity: count first, then delete based on counts. By separating the counting phase from the deletion phase, we avoid the complexity of trying to track duplicates while simultaneously modifying the list structure.

Learn more about Linked List patterns.

Solution Approach

The solution uses a hash table to count occurrences and a two-pass traversal strategy to delete nodes with duplicate values.

Step 1: Count Value Frequencies

First, we create a Counter object (hash table) to store the frequency of each value in the linked list:

cnt = Counter()
cur = head
while cur:
    cnt[cur.val] += 1
    cur = cur.next

This traversal visits each node once, incrementing the count for each value encountered. After this pass, cnt contains a mapping of each value to its frequency in the list.

Step 2: Set Up for Deletion

We create a dummy node that points to the head of the list:

dummy = ListNode(0, head)
pre, cur = dummy, head

The dummy node serves as a stable reference point before the actual head. We initialize two pointers:

  • pre: starts at the dummy node (always points to the node before cur)
  • cur: starts at the head (the current node being examined)

Step 3: Delete Nodes with Duplicate Values

We traverse the list again, checking each node's value against our frequency map:

while cur:
    if cnt[cur.val] > 1:
        pre.next = cur.next  # Skip the current node
    else:
        pre = cur           # Move pre forward only if we keep the node
    cur = cur.next          # Always move cur forward

The deletion logic works as follows:

  • If cnt[cur.val] > 1: The value appears multiple times, so we delete this node by updating pre.next = cur.next. Note that pre doesn't move forward in this case.
  • If cnt[cur.val] == 1: The value is unique, so we keep the node and advance pre to cur.
  • Regardless of deletion, cur always moves to the next node.

Step 4: Return the Modified List

Finally, we return dummy.next, which points to the new head of the modified list. This handles the case where the original head might have been deleted.

The time complexity is O(n) where n is the number of nodes (two passes through the list), and the space complexity is O(n) for the hash table storing value frequencies.

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 a concrete example to see how it works step by step.

Given linked list: 3 -> 1 -> 2 -> 3 -> 4 -> 2 -> 5

Step 1: Count Value Frequencies

We traverse the entire list and count each value:

  • Node with value 3: count becomes 1
  • Node with value 1: count becomes 1
  • Node with value 2: count becomes 1
  • Node with value 3: count becomes 2 (we've seen 3 before)
  • Node with value 4: count becomes 1
  • Node with value 2: count becomes 2 (we've seen 2 before)
  • Node with value 5: count becomes 1

After the first pass, our frequency map looks like:

cnt = {3: 2, 1: 1, 2: 2, 4: 1, 5: 1}

We've identified that values 3 and 2 appear twice, so all nodes with these values must be removed.

Step 2: Set Up for Deletion

We create a dummy node pointing to the head:

dummy -> 3 -> 1 -> 2 -> 3 -> 4 -> 2 -> 5
^pre     ^cur

Step 3: Delete Nodes with Duplicate Values

Now we traverse again, checking each node against our frequency map:

  1. cur = 3: cnt[3] = 2 > 1, so delete this node

    • Set pre.next = cur.next (dummy now points to 1)
    • Move cur to next node (1)
    • pre stays at dummy
    dummy -> 1 -> 2 -> 3 -> 4 -> 2 -> 5
    ^pre     ^cur
  2. cur = 1: cnt[1] = 1, so keep this node

    • Move pre to cur (pre now at node 1)
    • Move cur to next node (2)
    dummy -> 1 -> 2 -> 3 -> 4 -> 2 -> 5
             ^pre ^cur
  3. cur = 2: cnt[2] = 2 > 1, so delete this node

    • Set pre.next = cur.next (node 1 now points to 3)
    • Move cur to next node (3)
    • pre stays at node 1
    dummy -> 1 -> 3 -> 4 -> 2 -> 5
             ^pre ^cur
  4. cur = 3: cnt[3] = 2 > 1, so delete this node

    • Set pre.next = cur.next (node 1 now points to 4)
    • Move cur to next node (4)
    • pre stays at node 1
    dummy -> 1 -> 4 -> 2 -> 5
             ^pre ^cur
  5. cur = 4: cnt[4] = 1, so keep this node

    • Move pre to cur (pre now at node 4)
    • Move cur to next node (2)
    dummy -> 1 -> 4 -> 2 -> 5
                  ^pre ^cur
  6. cur = 2: cnt[2] = 2 > 1, so delete this node

    • Set pre.next = cur.next (node 4 now points to 5)
    • Move cur to next node (5)
    • pre stays at node 4
    dummy -> 1 -> 4 -> 5
                  ^pre ^cur
  7. cur = 5: cnt[5] = 1, so keep this node

    • Move pre to cur (pre now at node 5)
    • Move cur to next node (None)
    dummy -> 1 -> 4 -> 5 -> None
                       ^pre  ^cur

Step 4: Return Result

Return dummy.next, which gives us the final linked list: 1 -> 4 -> 5

All nodes with values that appeared more than once (3 and 2) have been completely removed from the list.

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 collections import Counter
8from typing import Optional
9
10class Solution:
11    def deleteDuplicatesUnsorted(self, head: Optional[ListNode]) -> Optional[ListNode]:
12        """
13        Remove all nodes that have duplicate values in an unsorted linked list.
14      
15        Args:
16            head: The head of the linked list
17          
18        Returns:
19            The head of the modified linked list with all duplicates removed
20        """
21        # First pass: Count frequency of each value in the linked list
22        frequency_map = Counter()
23        current = head
24        while current:
25            frequency_map[current.val] += 1
26            current = current.next
27      
28        # Create dummy node to simplify edge cases (removing head node)
29        dummy_node = ListNode(0, head)
30      
31        # Second pass: Remove nodes with frequency > 1
32        previous = dummy_node
33        current = head
34      
35        while current:
36            if frequency_map[current.val] > 1:
37                # Skip current node by updating previous node's next pointer
38                previous.next = current.next
39            else:
40                # Keep current node and move previous pointer forward
41                previous = current
42          
43            # Move to next node
44            current = current.next
45      
46        # Return the new head (dummy's next)
47        return dummy_node.next
48
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 deleteDuplicatesUnsorted(ListNode head) {
13        // Step 1: Count frequency of each value in the linked list
14        Map<Integer, Integer> frequencyMap = new HashMap<>();
15      
16        // Traverse the list to count occurrences of each value
17        for (ListNode current = head; current != null; current = current.next) {
18            frequencyMap.put(current.val, frequencyMap.getOrDefault(current.val, 0) + 1);
19        }
20      
21        // Step 2: Remove nodes that appear more than once
22        // Create a dummy node to simplify edge cases (e.g., removing the head)
23        ListNode dummy = new ListNode(0, head);
24      
25        // Use two pointers: previous and current
26        ListNode previous = dummy;
27        ListNode current = head;
28      
29        // Traverse the list again to remove duplicate values
30        while (current != null) {
31            if (frequencyMap.get(current.val) > 1) {
32                // Skip the current node by updating the previous node's next pointer
33                previous.next = current.next;
34            } else {
35                // Keep the current node and move the previous pointer forward
36                previous = current;
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* deleteDuplicatesUnsorted(ListNode* head) {
14        // First pass: Count frequency of each value in the linked list
15        unordered_map<int, int> frequencyMap;
16        for (ListNode* current = head; current != nullptr; current = current->next) {
17            frequencyMap[current->val]++;
18        }
19      
20        // Create a dummy node pointing to head to handle edge cases
21        // (e.g., when the first node needs to be deleted)
22        ListNode* dummyHead = new ListNode(0, head);
23      
24        // Second pass: Remove nodes that appear more than once
25        ListNode* previous = dummyHead;
26        ListNode* current = head;
27      
28        while (current != nullptr) {
29            if (frequencyMap[current->val] > 1) {
30                // Skip the current node by updating the previous node's next pointer
31                previous->next = current->next;
32            } else {
33                // Keep the current node and move the previous pointer forward
34                previous = current;
35            }
36            // Move to the next node
37            current = current->next;
38        }
39      
40        // Return the new head (dummy's next)
41        return dummyHead->next;
42    }
43};
44
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 an unsorted linked list that have duplicate values
15 * @param head - The head of the linked list
16 * @returns The head of the modified linked list with duplicates removed
17 */
18function deleteDuplicatesUnsorted(head: ListNode | null): ListNode | null {
19    // First pass: Count occurrences of each value in the linked list
20    const valueCountMap: Map<number, number> = new Map<number, number>();
21  
22    // Traverse the list and count each value
23    let currentNode: ListNode | null = head;
24    while (currentNode !== null) {
25        const nodeValue: number = currentNode.val;
26        const currentCount: number = valueCountMap.get(nodeValue) ?? 0;
27        valueCountMap.set(nodeValue, currentCount + 1);
28        currentNode = currentNode.next;
29    }
30  
31    // Create a dummy node to simplify edge cases (removing head node)
32    const dummyNode: ListNode = new ListNode(0, head);
33  
34    // Second pass: Remove nodes that appear more than once
35    let previousNode: ListNode = dummyNode;
36    let currentNodeToCheck: ListNode | null = head;
37  
38    while (currentNodeToCheck !== null) {
39        const occurrenceCount: number = valueCountMap.get(currentNodeToCheck.val)!;
40      
41        if (occurrenceCount > 1) {
42            // Skip this node by updating the previous node's next pointer
43            previousNode.next = currentNodeToCheck.next;
44        } else {
45            // Keep this node and move the previous pointer forward
46            previousNode = currentNodeToCheck;
47        }
48      
49        currentNodeToCheck = currentNodeToCheck.next;
50    }
51  
52    // Return the actual head (skipping the dummy node)
53    return dummyNode.next;
54}
55

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of two passes through the linked list:

  • First pass: Traverse the entire linked list to count the frequency of each value using a Counter (hash map). This takes O(n) time where n is the number of nodes in the linked list.
  • Second pass: Traverse the linked list again to remove nodes whose values appear more than once. This also takes O(n) time.

Since both passes are sequential and not nested, the overall time complexity is O(n) + O(n) = O(n).

Space Complexity: O(n)

The algorithm uses a Counter (hash map) to store the frequency of each unique value in the linked list. In the worst case, when all values in the linked list are unique, the Counter will store n key-value pairs, requiring O(n) space. The dummy node and pointer variables (pre, cur) only use constant extra space O(1), which doesn't affect the overall space complexity.

Therefore, the space complexity is O(n) where n is the length of the linked list.

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

Common Pitfalls

Pitfall 1: Incorrectly Advancing the previous Pointer

The Problem: A common mistake is to always advance the previous pointer in each iteration, regardless of whether a node was deleted:

# INCORRECT approach
while current:
    if frequency_map[current.val] > 1:
        previous.next = current.next
    else:
        previous.next = current  # Unnecessary
    previous = current  # WRONG: Always advancing previous
    current = current.next

This breaks the deletion logic because when we delete a node, previous should stay in place to potentially delete multiple consecutive nodes with duplicate values.

Example of the bug: For a list 1 -> 2 -> 2 -> 3 where 2 appears twice:

  • When we reach the first 2, we set previous.next = current.next (skipping it)
  • If we incorrectly advance previous to the deleted node's position, we lose our reference point
  • The second 2 won't be properly deleted

The Solution: Only advance previous when we keep a node:

while current:
    if frequency_map[current.val] > 1:
        previous.next = current.next  # Delete node
        # DON'T move previous here
    else:
        previous = current  # Only move previous when keeping a node
    current = current.next

Pitfall 2: Forgetting to Use a Dummy Node

The Problem: Without a dummy node, handling the deletion of the head node becomes complex:

# PROBLEMATIC approach without dummy node
if head and frequency_map[head.val] > 1:
    # Need special logic to handle head deletion
    while head and frequency_map[head.val] > 1:
        head = head.next
    # Then handle the rest of the list...

This leads to code duplication and edge case handling that makes the solution error-prone.

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

dummy_node = ListNode(0, head)
previous = dummy_node
current = head
# Now head deletion is handled uniformly with other deletions

Pitfall 3: Using the Wrong Data Structure for Counting

The Problem: Using a regular dictionary and forgetting to initialize values:

# INCORRECT: May cause KeyError
frequency_map = {}
current = head
while current:
    frequency_map[current.val] += 1  # KeyError if key doesn't exist
    current = current.next

The Solution: Use Counter from collections or properly initialize dictionary values:

# Option 1: Use Counter (recommended)
from collections import Counter
frequency_map = Counter()

# Option 2: Use defaultdict
from collections import defaultdict
frequency_map = defaultdict(int)

# Option 3: Check key existence
frequency_map = {}
while current:
    frequency_map[current.val] = frequency_map.get(current.val, 0) + 1
    current = current.next
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More