Remove Duplicates II: keep at most two of each
A sorted array, trimmed in place so no value appears more than twice — return k, the new length, with the answer packed into the first k slots and O(1) extra memory. Press Play on each figure and leave able to explain the one-pointer trick — not just type it.
Meet the three things you track
Before any code, just three characters. The whole algorithm is these three working together — once they click, the one-liner everyone memorizes reads itself. Step through and meet each one.
nums[r]. It walks left → right, visiting every spot once.nums. It comes in sorted, and we'll move the survivors to the front of this very same array — no second list, no extra memory.Sorted ⇒ duplicates are adjacent
Because the input is sorted, every copy of a value sits in one contiguous run. So the task isn't really "find duplicates anywhere" — it's "trim each run down to two." Watch the runs, then the survivors.
The catch: we don't want to find the runs first and copy survivors into a new array — that's O(n) extra space. We need to trim in place, deciding each element with a glance at what we've already kept.
Compare to the slot two back
Here is the single idea the whole problem turns on — and it's simpler than it sounds. You're allowed two of each value, so before you keep a new number, you just glance two spots back at what you've already kept. Step through it:
The full pass, step by step
Now the engine in motion on [0,0,1,1,1,1,2,3,3]. Follow the two pointers: r never stops, k only advances on a keep — and where they split is exactly the duplicates being discarded.
r advances and we evaluate the if — then, only if we keep, write & advance — we copy the value and bump k. So you'll see r and k move on different steps.It's all one dial
"At most two" was never special. Change the cap to m and the comparison index follows it to k − m. Flip the dial and watch the same code solve every version.
k−m move together, and the same five lines solve “remove duplicates” (m=1), “at most twice” (m=2), or any limit. That’s the whole pattern in one dial.The pattern, and its siblings
One reflex carries across a whole cluster: when you must rewrite an array in place, keep a slow write pointer and a fast read pointer, and let a tiny condition decide what gets written. These all rhyme.
nums[r] whenever it isn't the target value. k ends as the new length.nums[r] only when it differs from the last kept, nums[k − 1].[k, n) with zeros. Order preserved.The edge cases that break naive code
Four traps an interviewer will probe — each one handled by keeping the comparison anchored to k.
nums[r − 2] points into stale, not-yet-overwritten input. Always compare to nums[k − 2] — the kept region.k < 2 guard keeps them all and returns the length unchanged — no special-casing needed.[2,2,2,2,2] → keep the first two, skip the rest, return 2. A good sanity check that the cap actually fires.The interview cheat-sheet
r reads every slot, k writes the keepers. For each r, if k < 2 or nums[r] != nums[k − 2], write nums[k] = nums[r] and bump k. Return k. O(n) time, O(1) space.nums[r] == nums[k − 2], then nums[k − 2] and nums[k − 1] are already two copies of it — writing it would make a third.r − 2 drifts into stale data; k − 2 stays anchored to the answer.m," compare against nums[k − m]. m = 1 is plain remove-duplicates. Same five lines.r, write pointer k, both from 0. Keep nums[r] only when k < 2 or it differs from nums[k − 2] — the slot two behind the writer — because a sorted, deduped prefix means that match would be a third copy. One pass, in place."