Algorithm

Remove Duplicates from Sorted List

Linked List Pattern

Remove Duplicates from Sorted List

Given the head of a sorted (ascending) linked list, delete nodes so that each distinct value appears exactly once, keeping one copy of each. The result stays sorted. Modify the list in place and return its head — the head value is the smallest, so it is never the copy that gets removed, and the head pointer is unchanged. An empty list returns an empty list.

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,1,2]
Output: [1,2]
The value 1 appears twice in a row; one copy is kept and the repeat removed, leaving each distinct value once, still in sorted order.
EXAMPLE 2
Input: head = [1,1,2,3,3]
Output: [1,2,3]
Two separate runs of duplicates (the 1s and the 3s) each collapse to a single copy, so the surviving distinct values are 1, 2, 3.
EXAMPLE 3
Input: head = [1,1,1]
Output: [1]
All three nodes share the value 1, so only one copy remains. A run longer than two still reduces to exactly one node.
Is it guaranteed all equal values are adjacent?
Yes — that follows from the list being sorted. If it weren't sorted you could not rely on adjacency, and you'd need a set of seen values (or to sort first), which costs extra space.
Do I keep one copy of each value, or remove every node that was ever duplicated?
This problem keeps one copy of each distinct value. Removing all nodes that had any duplicate is a different, harder variant (Remove Duplicates II) — worth confirming which one the interviewer means.
Should I edit node values or unlink nodes?
Unlink nodes by rewiring next pointers; leave the surviving nodes' values untouched. Rewiring is the point — it's O(1) per removal and preserves node identity.
Can the head itself get removed?
Not in this version: the head holds the smallest value and we always keep the first copy of a value, so the head pointer never changes — you can safely return the original head.

We are handed a linked list whose values are already sorted, and we must throw away repeats so each value survives exactly once — [1, 1, 2] becomes [1, 2]. We only walk forward (the traversal from the previous problem), and we want to do it in place, without building a second list.

The honest first idea, and the word that makes it wasteful

If the list were in any order, duplicates could be anywhere — a second 1 might sit ten nodes away from the first. To catch that, you would carry a set of values already seen: at each node, ask "have I seen this value before?"; if yes, splice the node out; if no, record it. A set answers that question in roughly constant time, so this runs in O(N) time — but it spends O(N) extra memory on the set, up to one entry per distinct value.

python
seen = set()
prev, curr = None, head
while curr:
    if curr.val in seen:
        prev.next = curr.next      # splice out the repeat
    else:
        seen.add(curr.val)
        prev = curr
    curr = curr.next

Now look hard at what that set is buying us. It exists to answer "have I seen this value?" for values that might appear far apart. But the problem swore the list is sorted. In a sorted list, equal values cannot be far apart — they are physically forced to sit in one unbroken run, immediate neighbors of each other. (If two 1s had anything other than a 1 between them, that middle value would be both ≥ 1 and ≤ 1, i.e. also a 1 — so there is no gap.) The set is remembering, at O(N) cost, a fact the ordering already guarantees for free. That is the waste.

The reframe: sorted means the only duplicate you can meet is the next node

Because every group of equal values is one contiguous run, you never have to remember the past. Standing on any node, the only place a duplicate of its value can hide is directly after it. So the entire memory of "what have I seen" collapses to a single comparison: is my current value equal to my neighbor's? That comparison needs no set, no extra space — just the two nodes already in front of you.

The mechanism: skip the neighbor, but don't move yet

Keep one pointer curr. Compare curr.val to curr.next.val. If they are equal, the neighbor is a repeat, so bypass it: point curr.next to curr.next.next, unlinking the duplicate. If they differ, the neighbor is a genuinely new value, so advance curr onto it.

python
curr = head
while curr and curr.next:
    if curr.val == curr.next.val:
        curr.next = curr.next.next   # drop the duplicate; DO NOT advance
    else:
        curr = curr.next             # new value; safe to step forward
return head
Crucial Noteafter dropping a duplicate we deliberately do not advance curr — we stay put and compare against the new neighbor. This is the classic trap. Consider [1, 1, 1]: standing on the first 1 we drop the second, leaving 1 → 1. If we had stepped forward now, curr would land on the third 1 with nothing before it to compare against, and that third 1 would survive as a phantom duplicate. Staying put means we re-examine the same node until its neighbor is finally different, so a run of any length collapses to one. We advance only on a mismatch — the one moment we are certain the current value is done. The loop guard curr and curr.next matters for the same reason: the comparison reads curr.next.val, so we must confirm a neighbor exists before touching it, which also makes the empty list and single-node list exit immediately.
Worked Example:[1, 1, 2, 3, 3]
- curr on the first 1. Neighbor is 1 — equal. Drop it: list becomes 1 → 2 → 3 → 3. Stay on the first 1.
- curr still on 1. New neighbor is 2 — different. Advance to 2.
- curr on 2. Neighbor is 3 — different. Advance to 3.
- curr on the first 3. Neighbor is 3 — equal. Drop it: list becomes 1 → 2 → 3. Stay on 3.
- curr on 3. curr.next is now null — the guard fails. Stop. Result: [1, 2, 3].

Notice the two duplicate groups were handled with zero memory, and the run of two 3s at the very end still terminated cleanly because the guard checks for a neighbor first.

What we paid, and what we bought

Same single pass — O(N) time — but the set is gone, so space drops from O(N) to O(1). The whole saving came from reading the precondition: "sorted" quietly promised that equal things are adjacent, which turned a remember-everything problem into a compare-your-neighbor one. Train that reflex: when a problem hands you a sorted (or otherwise ordered) input, ask what that ordering makes adjacent, because adjacency is what lets you replace memory with a local comparison. And notice the exact contract here — we keep one copy of each value. The sibling problem that deletes every node that ever had a duplicate (leaving [1, 2, 3, 3] as just [1, 2]) needs a dummy node before the head and a slightly different guard; that variation is Remove Duplicates from Sorted List II.

Interactive Strategy Visualization

Pointer Reassignment

Optimization: O(1) Space
curr112
Current Actionnext.val == curr.val
Insight
Skipping duplicates in O(1) space
1. Check the Neighbor
We look at the current node (1) and its neighbor (1). They are the same, so we found a duplicate!
O(N) Time · O(N) Space Seen-Set
O(N) Time · O(1) Space Neighbor Compare