TL;DR
One pass with a dummy node: skip whole runs of repeated values, keep singletons β O(n) time, O(1) space.
Approach 1 β Brute force: count first, filter second
Two passes with a counter: tally every value, then rebuild the list keeping only values whose count is exactly 1.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
counts: Dict[int, int] = {}
node = head
while node:
counts[node.val] = counts.get(node.val, 0) + 1
node = node.next
dummy = ListNode()
tail = dummy
node = head
while node:
if counts[node.val] == 1:
tail.next = node
tail = node
node = node.next
tail.next = None
return dummy.next
Time O(n), space O(n) for the counter. With n <= 300 it passes easily β its real sin is ignoring the sortedness, spending O(n) memory on information the ordering gives you for free.
Approach 2 β One-pass run skipping with a dummy (optimal)
Insight: in a sorted list every duplicated value forms one consecutive run, so βappears more than onceβ is decidable locally: peek one node ahead. Keep prev pointing at the last node guaranteed to survive. If the run starting at prev.next has length >= 2, bypass the entire run in one splice; prev itself must not move, because the node right after the run might start another duplicate run. A dummy node in front of the head gives the head a predecessor, so head deletion needs no special case.
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head)
prev = dummy
node = head
while node:
if node.next and node.next.val == node.val:
run_val = node.val
while node and node.val == run_val:
node = node.next # skip the whole run
prev.next = node # splice it out; prev stays put
else:
prev = node # singleton survives
node = node.next
return dummy.next
Walkthrough on [1, 2, 3, 3, 4, 4, 5]:
| node | run? | action | list via dummy |
|---|
| 1 | no | prev=1 | 1β2β3β3β4β4β5 |
| 2 | no | prev=2 | unchanged |
| 3 | yes (3,3) | skip both, prev.next=4 | 1β2β4β4β5 |
| 4 | yes (4,4) | skip both, prev.next=5 | 1β2β5 |
| 5 | no | prev=5 | 1β2β5 |
Output [1, 2, 5]. Note prev stayed at 2 across both deletions β exactly why it only advances on singletons.
Time O(n): node only ever moves forward. Space O(1).
Approach 3 β Recursion
Insight: the same run logic phrased self-referentially: if the head starts a run, the answer is deleteDuplicates(first node after the run); otherwise itβs the head followed by the answer for the rest. Elegant, and a common interview follow-up (βcan you write it recursively?β).
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
if head.val == head.next.val:
node = head.next
while node and node.val == head.val:
node = node.next
return self.deleteDuplicates(node) # drop the whole run
head.next = self.deleteDuplicates(head.next)
return head
Walkthrough on [1, 1, 1, 2, 3]: head 1 starts a run β skip to node 2 and recurse. Head 2 is a singleton β keep it, recurse on [3]. [3] returns itself. Result 2β3.
Time O(n), space O(n) for the call stack (each frame consumes at least one node) β fine for n <= 300, but the iterative version wins on unbounded input.
Common pitfalls
- Advancing
prev after deleting a run β the next run may also need deleting, and prev must still be the splice point (see the double deletion in the walkthrough).
- Solving the wrong problem: keeping one copy per value is Remove Duplicates I; here duplicated values vanish entirely.
- No dummy node:
[1, 1, 2] deletes the head, and returning the right head without a dummy takes ugly special-casing.
- Comparing
node.next.val without first checking node.next is not None β instant crash on the last node.
Pattern takeaway
Two staples combine here. First: any deletion that might remove the head calls for a dummy predecessor node. Second: in sorted sequences, properties like βis duplicatedβ become run-local β process a whole run per step and the pass stays linear. Keep a βlast certain survivorβ pointer and only advance it when the node ahead is proven safe.