Remove Duplicates from Sorted List II — Delete All Occurrences with Dummy Head
Advertisement
Problem Statement
Given the head of a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list. Return the linked list sorted as well.
Constraints:
- The number of nodes in the list is in the range
[0, 300] -100 <= Node.val <= 100- The list is guaranteed to be sorted in ascending order
Example 1:
Input: head = [1, 2, 3, 3, 4, 4, 5]
Output: [1, 2, 5]
Explanation: All nodes with values 3 and 4 (which appeared more than once) are removed.Example 2:
Input: head = [1, 1, 1, 2, 3]
Output: [2, 3]
Explanation: All nodes with value 1 are removed.Example 3:
Input: head = [1, 1]
Output: []Why This Problem Matters
Remove Duplicates from Sorted List II (LeetCode 82) is the harder sibling of LC 83. The difference: LC 83 keeps one copy of each duplicate value; LC 82 removes all copies. This distinction changes the algorithm fundamentally. In LC 83, when you find a duplicate, you skip one node. In LC 82, when you find any duplicate, you must skip all nodes with that value and not keep any.
Google, Amazon, and Bloomberg ask this problem in interviews because it tests the predecessor-skip pattern with a more complex skip condition. You need a dummy head (because the head itself might be deleted), a predecessor pointer prev, and a two-level while loop: one to advance prev, and one nested inside to skip all duplicates of a given value.
The problem also illustrates an important interview skill: recognizing when you cannot advance the predecessor pointer. In LC 83, prev always advances. In LC 82, prev only advances if the current node is unique — it stays put while you skip all duplicates.
The Core Insight
Use a dummy head node before the real head. Keep prev pointing to the last confirmed unique node (or the dummy initially). Inspect prev.next:
- If
prev.nextandprev.next.nexthave the same value, it's a duplicate group. Record the duplicate value and skip all nodes with that value by walkingcurrthrough them, then setprev.next = curr(the first node after the duplicate group). - If
prev.next.nextdoesn't exist or has a different value,prev.nextis a unique node — advanceprev = prev.next.
Repeat until prev.next is None.
Visual Dry Run
Input: 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5
Setup: dummy -> 1 -> 2 -> 3 -> 3 -> 4 -> 4 -> 5, prev = dummy
| Step | prev | prev.next | prev.next.next | Duplicate? | Action |
|---|---|---|---|---|---|
| 1 | dummy | 1 | 2 | No (1≠2) | advance prev: prev=1 |
| 2 | 1 | 2 | 3 | No (2≠3) | advance prev: prev=2 |
| 3 | 2 | 3 | 3 | Yes (3=3) | skip all 3s: curr walks 3->3->4; prev.next=4 |
| 4 | 2 | 4 | 4 | Yes (4=4) | skip all 4s: curr walks 4->4->5; prev.next=5 |
| 5 | 2 | 5 | None | No (only one 5) | advance prev: prev=5 |
| 6 | 5 | None | — | Loop exits |
Return dummy.next = 1 -> 2 -> 5. Correct.
Head-deletion case: 1 -> 1 -> 2
| Step | prev | prev.next | prev.next.next | Action |
|---|---|---|---|---|
| 1 | dummy | 1 | 1 | Yes (1=1) — skip all 1s: curr=2; prev.next=2 |
| 2 | dummy | 2 | None | No — advance prev: prev=2 |
| 3 | 2 | None | — | Loop exits |
Return dummy.next = 2. Correct — head was deleted.
Solution (Optimal)
Python
def deleteDuplicates(head):
dummy = ListNode(0)
dummy.next = head
prev = dummy # last confirmed unique node (or dummy)
while prev.next:
curr = prev.next
# Check if curr starts a duplicate group
if curr.next and curr.val == curr.next.val:
# Skip all nodes with this duplicate value
dup_val = curr.val
while curr and curr.val == dup_val:
curr = curr.next
prev.next = curr # skip entire duplicate group
# Do NOT advance prev — the new prev.next might also be a duplicate
else:
prev = prev.next # curr is unique, advance prev
return dummy.nextTime complexity: O(n) — each node is visited at most twice (once by curr, once when prev passes through).
Space complexity: O(1) — dummy node and two pointer variables.
JavaScript
var deleteDuplicates = function(head) {
const dummy = new ListNode(0);
dummy.next = head;
let prev = dummy;
while (prev.next !== null) {
const curr = prev.next;
if (curr.next !== null && curr.val === curr.next.val) {
const dupVal = curr.val;
let c = curr;
while (c !== null && c.val === dupVal) {
c = c.next;
}
prev.next = c; // skip all duplicates
} else {
prev = prev.next; // unique node, advance
}
}
return dummy.next;
};Complexity:
| Metric | Value |
|---|---|
| Time | O(n) |
| Space | O(1) |
Common Mistakes
1. Confusing LC 83 (keep one) with LC 82 (remove all).
LC 83: when curr.val == curr.next.val, skip curr.next only. Keep curr.
LC 82: when curr.val == curr.next.val, skip ALL nodes with that value including curr itself.
2. Advancing prev after a skip.
After skipping a duplicate group and setting prev.next = curr, do NOT advance prev. The new prev.next might be another duplicate group (e.g., 1->1->2->2->3). Staying at prev allows the next iteration to check again.
3. Not using a dummy head.
If the first node is part of a duplicate group (e.g., [1, 1, 2]), the head changes. Without a dummy, you need special logic to find the new head. The dummy makes prev the predecessor of every node, including the first one.
4. Using prev.next and prev.next.next without null checks.
The while prev.next outer condition ensures prev.next exists. But inside, you must check curr.next (= prev.next.next) before accessing curr.next.val. The if curr.next and curr.val == curr.next.val condition handles this.
5. Using a sentinel value for dup_val instead of recording it.
After the inner while loop, curr has moved past all duplicates. You need curr to know where to reconnect. Don't try to use curr.val at the end — curr is already past the duplicates.
Interview Tips
-
State the key difference from LC 83: "In LC 83, when I find a duplicate, I skip the extra copy. In LC 82, I skip the entire duplicate group — even the first occurrence."
-
Explain why
prevdoesn't advance after a skip: "After skipping, the newprev.nextcould be another duplicate group. I need to check again before advancingprev." -
Walk the head-deletion case:
[1, 1, 2]— show that the dummy handles it: prev stays at dummy, skips both 1s, advances to 2. -
Use the inner while loop explicitly: Show that you skip all nodes with
dup_val, not just one. This handles triples and longer runs. -
Test
[1, 1]: After skipping, prev.next = None. Returndummy.next = None. Empty list. Correct.
Follow-up Questions
Q: What's the difference between LC 83 and LC 82 in code?
LC 83 uses if curr.val == curr.next.val: curr.next = curr.next.next (skip one copy). LC 82 uses an inner while loop to skip all copies and requires a predecessor pointer that doesn't advance after a skip.
Q: What if the list is not sorted? With an unsorted list, duplicates aren't consecutive. You'd need a hash set: two passes — first pass records all values that appear more than once, second pass removes nodes with those values. O(n) time and O(n) space.
Q: Can you do it recursively?
Yes: deleteDuplicates(head). If head and head.next have the same value, skip all with that value and recurse: return deleteDuplicates(non_dup_head). If unique: head.next = deleteDuplicates(head.next); return head. O(n) stack space.
Q: What if values can be negative? The algorithm doesn't assume positive values — it compares values for equality. Negative values work correctly.
Q: Can you solve it in one line with Python list comprehension? Not directly on a linked list, but you could collect values, filter for non-duplicates, and rebuild. That's O(n) time and O(n) space — less efficient and defeats the purpose of practicing linked list manipulation.
Key Takeaways
- Use a dummy head — the original head might be deleted.
previs the last confirmed unique predecessor. It only advances when the current node is confirmed unique.- When a duplicate is found, use an inner while loop to skip ALL nodes with that value — not just one.
- Do NOT advance
prevafter a skip — the newprev.nextmight also need skipping. - This is the harder sibling of LC 83 — the key difference is "remove all" vs "keep one."
- Time O(n), Space O(1) — single pass with a predecessor pointer and inner skip loop.
Advertisement