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 be1 -> 2 -> 5
(both3
and4
appear multiple times, so all occurrences are removed) - If the input linked list is
1 -> 1 -> 1 -> 2 -> 3
, the output should be2 -> 3
(the value1
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.
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 meanscur
didn't move forward (no duplicates were found), so this node should be kept. We movepre
tocur
. - If
pre.next != cur
, it meanscur
moved forward (duplicates were found), so we need to skip all these duplicate nodes by settingpre.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 setdummy.next = head
to handle edge cases where the first nodes need to be removed - Initialize pointer
pre
pointing todummy
(tracks the last valid node to keep) - Initialize pointer
cur
pointing tohead
(explores the list to find duplicates)
Step 2: Traverse the linked list
While cur
is not null:
-
Find all duplicates of current value
- While
cur.next
exists andcur.next.val == cur.val
:- Move
cur
forward:cur = cur.next
- Move
- This loop positions
cur
at the last node of a sequence of duplicate values
- While
-
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
- Move
- 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
- Connect
- If true: No duplicates were found (cur didn't move), so this node should be kept
- Check if
-
Move to next node
- Move
cur
forward:cur = cur.next
- Move
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, thenpre.next = cur.next
(connects 2 to 4) - Node 4: Found duplicate (next is also 4),
cur
moves to second 4, thenpre.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 EvaluatorExample 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 dummycur
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 2
s, if the logic doesn't correctly position the pointers, it might fail to detect the 3
s 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 1
s 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.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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
Want a Structured Path to Master System Design Too? Don’t Miss This!