1836. Remove Duplicates From an Unsorted Linked List 🔒
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:
- 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. - 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.
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 beforecur
)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 updatingpre.next = cur.next
. Note thatpre
doesn't move forward in this case. - If
cnt[cur.val] == 1
: The value is unique, so we keep the node and advancepre
tocur
. - 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 EvaluatorExample 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:
-
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
- Set
-
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
-
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
- Set
-
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
- Set
-
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
-
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
- Set
-
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 takesO(n)
time wheren
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 setprevious.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
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!