Remove Duplicates from Sorted Array II
Remove Duplicates from Sorted Array II
Given a sorted integer slice, remove duplicates in-place so that each value appears at most twice. Return the new length k such that the first k elements contain the allowed values in sorted order.
Why this is worth learning
The decision depends on the compacted output, not on a count of the last two input positions. The same argument generalizes to keeping at most k copies.
Before coding: one-minute experiment
Why does comparing nums[read] with nums[read-2] fail on [1,1,1,2,2,2,3] after compaction?
Function signature
func RemoveDuplicatesSortedArrayII(nums []int) int
Inputs / outputs
nums: sorted slice to modify in-place.- Return: number of kept elements
k; onlynums[:k]is valid after the call.
Example
nums = [1,1,1,2,2,3]
After removal: nums = [1,1,2,2,3,_]
Return k = 5
Constraints
0 <= len(nums) <= 100_000-1_000_000_000 <= nums[i] <= 1_000_000_000
Core idea (near-solution)
Because the array is sorted, any value beyond the second occurrence can be skipped. The easiest invariant uses the write index:
Invariant: nums[:write] contains the valid output so far, with no value appearing more than twice.
If write < 2, you can always keep the element. Otherwise, compare nums[read] to nums[write-2]:
- If they are different, the current value has appeared fewer than twice in the output, so keep it.
- If they are the same, this would be a third occurrence - skip it.
Algorithm (step-by-step)
- If
len(nums) <= 2, returnlen(nums). - Set
write = 2. - For
readfrom2tolen(nums)-1:- If
nums[read] != nums[write-2], setnums[write] = nums[read]and incrementwrite.
- If
- Return
write.
Correctness sketch
For length at most two, every value is allowed. Thereafter nums[:w] is the correctly capped prefix of the values already scanned. If the new value equals nums[w-2], sortedness implies the last two kept values both equal it; another copy would violate the cap. If it differs, fewer than two copies of this value are in the kept prefix, so appending is valid. Each accepted value is written no further right than its read position. Induction over the scan establishes the exact sorted prefix and returned length.
Trace (hand walkthrough)
Example input:
nums = [1,1,1,2,2,3]
After removal: nums = [1,1,2,2,3,_]
Return k = 5
Walk:
- start with first two kept: [1,1], write=2
- i=2 val=1, compare nums[write-2]=1 -> skip (would make 3 copies)
- i=3 val=2, compare nums[0]=1 -> keep -> nums[2]=2, write=3
- i=4 val=2, compare nums[1]=1 -> keep -> nums[3]=2, write=4
- i=5 val=3, compare nums[2]=2 -> keep -> nums[4]=3, write=5 => [1,1,2,2,3]
Pitfalls to avoid
- Using the wrong comparison index (the
write-2check is key). - Failing to handle arrays of length 0, 1, or 2.
- Returning a new slice instead of the new length.
Debugging lens
Trace write separately from read. Use runs of lengths 1, 2, 3, and 5 in the same input so that a mistaken read-2 comparison cannot hide behind uniform runs.
Complexity
- Time:
O(n) - Extra space:
O(1)
Implementation notes (Go)
- This approach preserves order and is stable.
Reconstruction checkpoint
Why does comparing nums[read] with nums[read-2] fail on [1,1,1,2,2,2,3] after compaction?
Model answer
After skipping the third 1, read=3 writes the first 2 at index 2 and sets write=3. At read=4, the wrong read-2 comparison sees that overwritten 2 at index 2 and rejects the second 2. The correct write-2 comparison reads index 1, which still holds 1, so it accepts the second copy. Input positions are not accepted-output history.