DSA sheet · Linked List · Merge / Sort pattern

Remove Duplicates from Sorted List II

The harder sister of Problem 19. There, we kept one copy of every value. Here, any value that appears more than once must disappear completely, every copy of it. The teacher first solves it with a hash map of frequencies (and shows a small off-by-one bug while writing values back), then builds the O(1)-space answer from scratch, discovering one by one why we need a previous pointer, why we need a dummy node, why there's a loop inside the loop, and why the previous pointer moves in two different ways.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Nodes, head, None

A linked list is a chain of nodes. Each node has a value (val) and a link to the next node (next). The last node points to None. The head is the first node; an empty list is head = None.

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
  [1] → [2] → [3] → [3] → [4] → [4] → [5] → None
   ↑
  head

Walking, and no index access

We walk with a separate pointer, curr = curr.next, so we never lose track of the start (the teacher always makes this "alias"). There's no list[i]; reaching position i means walking i steps, O(n). So a node can't "look back" at the node before it: if we'll need the previous node, we must carry a pointer to it ourselves. That's the heart of this problem.

Save next before you change a pointer

Overwriting node.next loses whatever it pointed to unless another pointer still holds it. Below, when we write prev.next = curr.next, the node we want to keep is read from curr.next in the same line, so nothing we need is lost; only the duplicates become unreachable.

Deleting a whole block of nodes

To delete a run of nodes, point the node just before the run to the node just after it. One assignment removes the whole block:

  [2] → [3] → [3] → [4] → ...
   ↑                 ↑
  prev       node after the block

  prev.next = (node after the block)

  [2] ─────────────→ [4] → ...      both 3s are gone

Tool: the dummy node

A dummy is a fake node we put in front of the head: dummy = ListNode(0, head). Why? In this problem the head itself might be deleted (in 1 → 1 → 1 → 3 the answer starts at 3). Without a dummy, deleting the first nodes would need special code to "change the head". With a dummy, the first real node has a node before it like every other node, so the same prev.next = … line works everywhere, and at the end the true head is always dummy.next (even if that's None because everything was deleted).

Part A · Brute force: frequency map + overwrite

LeetCode 82

1The question in simple words

You get the head of a sorted linked list. Delete all nodes whose value appears more than once, leaving only values that appear exactly once. Return the (still sorted) list.

  input:  [1] → [2] → [3] → [3] → [4] → [4] → [5] → None
  output: [1] → [2] → [5] → None

  input:  [1] → [1] → [1] → [2] → [3] → None
  output: [2] → [3] → None              ← the head itself was deleted
inputProblem 19 (keep one copy)Problem 22 (remove all copies)
1, 2, 3, 3, 4, 4, 51, 2, 3, 4, 51, 2, 5

2What the constraints tell us

3Intuition: count, then keep the count-1 values

"Duplicate" means "frequency more than 1". So count how many times each value appears. Then go through the values and keep only those with count exactly 1.

For 1, 2, 3, 3, 4, 4, 5: counts are 1→1, 2→1, 3→2, 4→2, 5→1. Keep 1, 2, 5.

Doubt: why a hash map here, when Problem 19 didn't need one?
→ In Problem 19 we kept one node of each value, so "is it the same as the last one I kept?" was enough. Here we must know whether a value has any other copy, including the first copy we already walked past. A frequency count answers that directly.

4Building the logic

Fill the map

Walk the list. For each value: freq[v] = freq.get(v, 0) + 1. The first time a value appears, it isn't in the map, so get gives the default 0, and we store 1. The second 3 reads 1 and stores 2.

Collect the unique values

Walk the list again and append curr.val to kept when freq[curr.val] == 1. (You could loop over the map instead. In Python a dict remembers insertion order, and we inserted in sorted list order, so that also gives sorted output. In Java a HashMap has no order, which is why walking the list is the safer habit.)

If nothing is kept

Example 1 → 1 → 1 → 2 → 2 → 3 → 3: counts are 3, 2, 2, so kept is empty. There's nothing to build; return None right away.

Write back, and the off-by-one bug

Like Problem 19, overwrite the first len(kept) nodes with the kept values, then end the list. The teacher's first attempt: write a value, move curr, repeat; after the loop set curr.next = None. But after writing the last value, curr has already moved one node further:

  kept = [1, 2, 5]
  after writing 3 values:  [1] → [2] → [5] → [3] → [4] → [4] → [5] → None
                                             ↑
                                    curr is HERE (one too far)

  curr.next = None  →  [1] → [2] → [5] → [3] → None     wrong: an extra 3

Her fix: keep a prev pointer one node behind curr (set prev = curr just before moving curr). After the loop, prev is on the last written node, so prev.next = None cuts in the right place.

  [1] → [2] → [5] ✂ [3] → [4] → [4] → [5]
               ↑     ↑
             prev   curr          prev.next = None  →  [1] → [2] → [5] → None ✓

5Approach steps

  1. If head is None → return None.
  2. Count every value in a dict.
  3. Walk the list; collect values with count 1 in kept.
  4. If kept is empty → return None.
  5. Overwrite the first len(kept) nodes, tracking prev; then prev.next = None. Return head.

6Code (Python)

Brute force: frequency map + overwrite
class Solution:
    def deleteDuplicates(self, head):
        if head is None:
            return None

        freq = {}
        curr = head
        while curr is not None:                     # count each value
            freq[curr.val] = freq.get(curr.val, 0) + 1
            curr = curr.next

        kept = []
        curr = head
        while curr is not None:                     # keep count-1 values, in order
            if freq[curr.val] == 1:
                kept.append(curr.val)
            curr = curr.next

        if not kept:                                # everything was duplicated
            return None

        prev = None
        curr = head
        for v in kept:                              # overwrite in place
            curr.val = v
            prev = curr
            curr = curr.next
        prev.next = None                            # cut after the last written node
        return head

7Code line by line

linewhat it means
if head is None: return NoneEmpty list allowed by the constraints.
freq[curr.val] = freq.get(curr.val, 0) + 1Read the old count (0 if new) and add one.
if freq[curr.val] == 1: kept.append(curr.val)Only values that appear exactly once survive. Walking the list keeps them sorted.
if not kept: return NoneEvery value had copies, so the answer is empty.
curr.val = v prev = curr curr = curr.nextWrite a kept value into the next node; remember that node as prev before stepping on.
prev.next = Noneprev is the last written node; end the list there (curr is one node too far).
return headSame first node, new values.

8Dry run: 1, 2, 3, 3, 4, 4, 5

phasestate
countfreq = {1: 1, 2: 1, 3: 2, 4: 2, 5: 1}
collectkept = [1, 2, 5]
writenodeprev · curr afterlist after
v = 1node 1node1 · node2[1] → [2] → [3] → [3] → [4] → [4] → [5]
v = 2node 2node2 · node3unchanged values so far
v = 5node 3node3 · node4[1] → [2] → [5] → [3] → [4] → [4] → [5]
cutnode3.next = None–[1] → [2] → [5] → None ✓

9Complexity & remember

RememberCount with a dict, keep the count-1 values, empty → None, overwrite, and cut with prev (not curr, which is one node too far).

Part B · Optimal: prev + curr + dummy, in place

1The question in simple words

Same question, but with no extra space: only a few pointers, changing links in the existing list. The teacher builds this one "as if she had never seen it", so let's follow her discoveries in order.

2What the constraints tell us

3Intuition: cut out whole blocks

Think of the sorted list as blocks of equal values: [1] [2] [3 3] [4 4] [5]. A block of size 1 stays. A block of size 2 or more is cut out entirely, by linking the node before the block to the node after it. So at every moment we need: a pointer to the last node we've decided to keep (prev) and a pointer that scans forward (curr).

4Building the logic, discovery by discovery

Example: 1 → 2 → 3 → 3 → 4 → 4 → 5.

Discovery 1: compare curr with curr.next, both must exist

As in Problem 19, a duplicate is found by checking curr.val == curr.next.val. Before reading curr.next.val, curr.next must not be None (Java would throw a null pointer exception, Python an AttributeError). 1 vs 2: different, move. 2 vs 3: different, move. 3 vs 3: same!

Discovery 2: Problem 19's trick leaves one copy behind

If we do curr.next = curr.next.next like before, the second 3 goes, and curr (first 3) now sees 4: different, move on. But the first 3 is still in the list, and this problem wants it gone too. To unlink that first 3, we need the node before it (the 2), and a list can't walk backwards. So we need a prev pointer that trails behind.

Discovery 3: the head can be deleted → use a dummy

Take 1 → 1 → 1 → 3. All the 1s go; the answer starts at 3. So "return head" is wrong whenever the head's value is duplicated. Rather than tracking "what's the new head?" with extra if/else, the teacher's preferred fix: a dummy before the head, prev starting at the dummy, and return dummy.next. If everything is deleted, dummy.next is None, which is right; if the first few blocks are deleted, dummy.next points to the first survivor.

  [0] → [1] → [2] → [3] → [3] → [4] → [4] → [5] → None
 dummy   ↑
  prev  curr

Discovery 4: skipping a block needs a loop, not a single step

When curr and curr.next match, move curr forward: curr = curr.next. What if there were three 3s? We'd check again and move again. "Repeat while true" means a while loop: while curr.next and curr.val == curr.next.val: curr = curr.next. When it stops, curr is on the last node of the block (the second 3), because the next node (4) is different.

  [0] → [1] → [2] → [3] → [3] → [4] → [4] → [5]
               ↑            ↑
              prev         curr  (last 3; curr.next = 4 differs)

Discovery 5: after the block, link prev past it

The inner loop moved only curr, never prev. curr itself is part of the duplicate block, so it must go too. prev (the 2) should now point to the node after curr: prev.next = curr.next. Then curr steps forward: curr = curr.next (onto the first 4). prev stays on 2, because we don't yet know whether 4 survives.

  [0] → [1] → [2] ───────────→ [4] → [4] → [5]
               ↑                ↑
              prev             curr

Next round: 4 vs 4 match → inner loop takes curr to the second 4 → prev.next = curr.next links 2 → 5 → curr moves to 5. prev is still on 2. Good: that's exactly why prev must not move after a deletion. The next block could also be a duplicate block, and prev must stay on the last node that's known to be kept.

Discovery 6: why the if around the while? Because prev moves in two ways

If curr and curr.next are different (no duplicate starts here), curr is a survivor. Then prev should take a normal step forward along with curr. That's the else branch, and it's why the teacher wraps the inner while in an if: she needs an else.

To show this, she imagines a 6 after the 5: at 5, 5 vs 6 differ → prev steps onto 5, curr onto 6; then 6 has no next → else again → prev onto 6, curr onto None → stop.

Discovery 7: the outer loop and the return

Repeat everything while curr is not None (if curr is None there's nothing left to check). Afterwards return dummy.next, never head, because the head might have been cut out.

Doubt: in the else branch, why prev = prev.next and not prev = curr?
→ They're the same node here. Every time we reach the top of the loop, prev.next is curr (at the start, dummy.next = head = curr; after a deletion we set prev.next = curr.next and then curr = curr.next; after a normal step both move by one). So either line works; the teacher writes prev = prev.next.

5Approach steps

  1. dummy = ListNode(0, head), prev = dummy, curr = head.
  2. While curr is not None:
    • if curr.next exists and has the same value: move curr to the last node of this block (inner while), then prev.next = curr.next;
    • else: prev = prev.next;
    • in both cases: curr = curr.next.
  3. Return dummy.next.

6Code (Python)

In place: dummy + prev + curr
class Solution:
    def deleteDuplicates(self, head):
        dummy = ListNode(0, head)     # in front of head (head may be deleted)
        prev = dummy                  # last node we are sure to keep
        curr = head                   # scanner

        while curr is not None:
            if curr.next is not None and curr.val == curr.next.val:
                # walk to the last node of this duplicate block
                while curr.next is not None and curr.val == curr.next.val:
                    curr = curr.next
                prev.next = curr.next     # jump over the whole block
            else:
                prev = prev.next          # curr survives: prev steps normally
            curr = curr.next

        return dummy.next

7Code line by line

linewhat it means
dummy = ListNode(0, head)A fake node before head, so deleting the first block is not a special case.
prev = dummyprev = the last node we've confirmed as a survivor. At the start that's the dummy.
while curr is not None:Check every block until the end.
if curr.next is not None and curr.val == curr.next.val:A duplicate block starts at curr. The None check comes first so .val is safe.
while curr.next is not None and curr.val == curr.next.val: curr = curr.nextSlide curr to the last copy, however many copies there are.
prev.next = curr.nextUnlink the whole block in one go: prev now points to the first node after it.
else: prev = prev.nextNo duplicate here, so curr is kept; prev moves onto it.
curr = curr.nextGo to the next block's first node (both branches).
return dummy.nextThe real head, which may differ from the original head, or be None.

8Dry run: 1 → 2 → 3a → 3b → 4a → 4b → 5

roundprev · curr (start of round)checkwhat changeslist from dummy afterkept so far
1dummy · 11 ≠ 2prev → 1, curr → 20 → 1 → 2 → 3 → 3 → 4 → 4 → 51
21 · 22 ≠ 3prev → 2, curr → 3asame1, 2
32 · 3a3 = 3inner: curr → 3b; 2.next = 4a; curr → 4a0 → 1 → 2 → 4 → 4 → 51, 2
42 · 4a4 = 4inner: curr → 4b; 2.next = 5; curr → 50 → 1 → 2 → 51, 2
52 · 55.next is Noneelse: prev → 5, curr → Nonesame1, 2, 5
endcurr is None → return dummy.next = 1 → 1 → 2 → 5 ✓
  start:        [0] → [1] → [2] → [3a] → [3b] → [4a] → [4b] → [5] → None
               prev  curr

  round 3:      [0] → [1] → [2] → [3a] → [3b] → [4a] → [4b] → [5]
                             prev          curr  (after inner loop)
                2.next = 4a:
                [0] → [1] → [2] ─────────────→ [4a] → [4b] → [5]
                             prev               curr

  round 4:      [0] → [1] → [2] ──────────────────────────→ [5] → None
                             prev                            curr

  round 5:      prev → 5, curr → None.  Answer: [1] → [2] → [5] → None

When the head is deleted: 1a → 1b → 1c → 2 → 3.

roundprev · currcheckwhat changeslist from dummy after
1dummy · 1a1 = 1inner: curr → 1b → 1c; dummy.next = 2; curr → 20 → 2 → 3
2dummy · 22 ≠ 3prev → 2, curr → 3same
32 · 3no nextprev → 3, curr → Nonesame
endreturn dummy.next = 2 → 3 ✓ (returning head would give the deleted 1a)

All duplicates, e.g. 1 → 1 → 2 → 2: round 1 sets dummy.next = 2a, round 2 sets dummy.next = None → return None ✓.

9Complexity & remember

Rememberdummy before head; prev = last kept node; curr scans. Duplicate starts → slide curr to the block's end, prev.next = curr.next (prev stays). Otherwise prev steps. Always curr = curr.next. Return dummy.next.

Part C · Revision page

Frequency mapprev + curr + dummy
finds duplicates bycount > 1curr.val == curr.next.val (sorted)
removes byoverwriting values, cutting the tailrelinking prev past each block
timeO(n)O(n)
extra spaceO(n) map (+ list)O(1)
Problem 19 (keep one)Problem 22 (remove all)
pointerscurr onlyprev + curr + dummy
on a duplicatecurr.next = curr.next.nextslide to block end, prev.next = curr.next
can the head change?no → return headyes → return dummy.next
If you remember only 5 lines 1. "Remove all copies" → you need the node before the block → prev.
2. The head may be deleted → dummy in front, return dummy.next.
3. Duplicate block: inner while slides curr to the last copy.
4. Then prev.next = curr.next, and prev does not move.
5. No duplicate: prev steps. Always curr steps. O(n), O(1).
Mistakes to avoid ✗ using Problem 19's single skip → one copy survives
✗ returning head → wrong when the first block is deleted
✗ moving prev after a deletion → the next duplicate block can't be removed
✗ if instead of the inner while → blocks of 3+ break
✗ brute force: cutting at curr instead of prev (one extra node stays)
test it yourself (paste under either solution)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

def build(vals):
    dummy = tail = ListNode()
    for v in vals:
        tail.next = ListNode(v)
        tail = tail.next
    return dummy.next

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
print(to_list(s.deleteDuplicates(build([1, 2, 3, 3, 4, 4, 5]))))  # [1, 2, 5]
print(to_list(s.deleteDuplicates(build([1, 1, 1, 2, 3]))))        # [2, 3]
print(to_list(s.deleteDuplicates(build([1, 1, 2, 2]))))           # []
print(to_list(s.deleteDuplicates(build([]))))                     # []

Based on this video: Remove Duplicates from Sorted List II