Remove Duplicates from Sorted List II: Delete All Duplicated Nodes
Remove Duplicates from Sorted List II is LeetCode 82. 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.
This is not the same as removing extra copies of a value. If a value appears more than once, you remove every node with that value. The number is gone entirely.
Example 1: [1, 2, 3, 3, 4, 4, 5] becomes [1, 2, 5]
Example 2: [1, 1, 1, 2, 3] becomes [2, 3]
The challenge
The tricky part is that duplicates might start at the head. If the list is [1, 1, 2, 3], the head itself needs to be removed. That means you cannot just start iterating from head and keep a previous pointer, because there is no node before the head to link from.
You need a way to always have a "previous" node available, even when the head is being deleted. That is where the dummy head technique comes in.
The approach
The algorithm uses two key ideas:
-
Dummy head. Create a sentinel node that sits before the real head. Its
nextpoints tohead. This guarantees thatprevalways has a valid node to work from, even if every real node gets deleted. -
Skip-duplicates scan. Walk through the list with a
currpointer. At each position, check ifcurrandcurr.nextshare the same value. If they do, keep advancingcurruntil you reach a node with a different value (or null). Then setprev.next = currto skip over the entire duplicate group. Ifcurris unique, simply advanceprevtocurr.
The key insight: prev only advances when curr is confirmed unique. When duplicates are found, prev stays put while curr scans forward. Then prev.next is rewired to jump past all the duplicate nodes.
Visual walkthrough
Let's trace through [1, 2, 3, 3, 4, 4, 5] step by step. D is the dummy node.
Initial: prev = dummy, curr = head (node 1)
D is the dummy node. prev starts at dummy, curr starts at head.
curr.val (1) != curr.next.val (2). No duplicates. Advance prev and curr.
Node 1 is unique. prev moves to 1, curr moves to 2.
curr.val (2) != curr.next.val (3). No duplicates. Advance prev and curr.
Node 2 is unique. prev moves to 2, curr moves to 3.
curr.val (3) == curr.next.val (3). Duplicate found! Skip all 3s.
Scan past all nodes with value 3. Set prev.next = curr (node 4). prev stays at 2.
curr.val (4) == curr.next.val (4). Another duplicate! Skip all 4s.
Scan past all nodes with value 4. Set prev.next = curr (node 5). prev stays at 2.
curr.next is null. Node 5 is unique. Done.
Return dummy.next. The result is 1 -> 2 -> 5.
Notice how prev stays at node 2 while both groups of duplicates (the 3s and the 4s) get skipped. The dummy node handles the case where duplicates appear at the head.
The solution
def deleteDuplicates(head):
dummy = ListNode(0, head)
prev = dummy
curr = head
while curr:
if curr.next and curr.val == curr.next.val:
while curr.next and curr.val == curr.next.val:
curr = curr.next
prev.next = curr.next
curr = curr.next
else:
prev = curr
curr = curr.next
return dummy.next
The outer while walks every node. The inner while handles the skip: when a duplicate is found, it advances curr until it reaches the last node of the duplicate group. Then prev.next jumps to curr.next, cutting out every node in that group. The curr = curr.next after the skip moves curr to the first node after the group so the outer loop can continue.
Pay attention to the inner loop condition: curr.next and curr.val == curr.next.val. After this loop, curr sits on the last duplicate node. Setting prev.next = curr.next skips the entire group. Then curr = curr.next resumes scanning from the node right after the group.
Complexity analysis
Time: O(n). Each node is visited at most twice: once by the outer loop and possibly once by the inner skip loop. The two loops together cover each node a constant number of times.
Space: O(1). Only a dummy node and a few pointers. No extra data structures.
| Approach | Time | Space |
|---|---|---|
| Dummy head + skip scan | O(n) | O(1) |
| Collect unique values in set, rebuild list | O(n) | O(n) |
Edge cases
- All duplicates. Every value appears more than once, like
[1, 1, 2, 2, 3, 3]. The dummy node saves you here.prevstays at the dummy the entire time, anddummy.nextends up asnull. An empty list is returned. - No duplicates. Every value is unique, like
[1, 2, 3]. The inner loop never fires.prevadvances withcurrthrough the entire list. The original list is returned unchanged. - Single node. Only one node, like
[1]. The outer loop runs once, finds no duplicate (curr.nextisnull), and advances normally. The single node is returned. - Duplicates at the head.
[1, 1, 2, 3]. Without the dummy node, you would lose your entry point. The dummy handles this:prevstays at the dummy while the 1s are skipped, thendummy.nextpoints to node 2.
The building blocks
This problem is built from two reusable techniques.
Dummy head technique. Adding a sentinel node before the real head eliminates the special case of modifying or deleting the first node. Instead of writing separate logic for "what if the head changes," you always have a prev pointer that is safe to update. After the algorithm finishes, dummy.next is the real head. This pattern appears in Merge Two Sorted Lists, Remove Nth Node From End, and anywhere the head might change.
Skip-duplicates scan. A while loop that advances through a group of consecutive equal values until it reaches a different value. This is the same scan you use in Remove Duplicates from Sorted Array and its variations. The sorted input guarantees that all duplicates are adjacent, so a single forward scan finds them all.
These two bricks combine naturally. The dummy head gives you a stable anchor. The skip scan identifies and removes duplicate groups. Together they solve the problem in one clean pass.
From understanding to recall
The logic of this problem is not hard to follow. You scan forward, skip duplicates, rewire pointers. Reading the solution, it makes sense. But under interview pressure, the details trip people up. Which pointer do you advance after a skip? When does prev move versus stay? Where exactly does prev.next point after the inner loop?
The fix is not re-reading. It is typing the solution from scratch, multiple times. The first attempt you will probably get the inner loop boundary wrong. By the third or fourth attempt, the two-loop structure and the prev/curr dance become automatic. That is when this problem stops being a source of bugs and starts being a free five minutes in an interview.
Related posts
- Remove Duplicates from Sorted List - The easier variant (LeetCode 83) where you keep one copy of each duplicate instead of removing all copies
- Reverse Linked List - The fundamental linked list pointer manipulation technique