Course Content
Coding Interview Patterns
20 sections · 146 lessons
Remove Duplicates from Sorted Array
This is the parallel arrangement in its cleanest form. Both pointers start at the left and move right, but at different speeds: one reads every element, the other writes only the ones worth keeping. Interviewers like it because the phrase "in place, return the new length" tests whether you know this template, and because two small variants — "keep two copies", "move zeroes" — check whether you understand it or memorised it.
The problem
You are given a list of integers sorted in ascending order. Rearrange it in place so that each distinct value appears exactly once at the front, in the original order, and return how many distinct values there are. What sits beyond that count does not matter.
[1, 1, 2, 2, 3]→3, with the list starting[1, 2, 3, ...].[3, 3, 3, 5, 8, 8]→3, with the list starting[3, 5, 8, ...].
Constraints: 0 ≤ len(numbers) ≤ 10⁵; sorted ascending. Use O(1) extra space — you may not build a second list.
Clarifying questions
- Must the tail be cleaned or truncated? No — only the first
kslots are checked. (In Python you coulddel numbers[k:], but it is not required.) - Empty list? Allowed; return 0.
- Is it really sorted? Yes. Sorting is what puts all copies of a value next to each other, and the solution depends on it.
Approach 1: the simple way
Walk the list and delete each element that equals the one before it.
1def remove_duplicates_brute(numbers: list[int]) -> int:2 """Delete each repeat where it stands. Every pop shifts the tail left."""3 i = 14 while i < len(numbers):5 if numbers[i] == numbers[i - 1]:6 numbers.pop(i)7 else:8 i += 19 return len(numbers)It is correct and in place, but numbers.pop(i) shifts every later element one slot left. With many duplicates that is O(n) work per deletion, so O(n²) time overall. On a list of 100,000 copies of one value it took about 1.2 seconds on our test machine, against about 2 milliseconds for the two-pointer version. (Building a new list of unique values would be O(n) time but O(n) space, which the problem forbids.)
The key insight
You do not need to delete anything. You only need the first k slots to hold the answer. So split the job between two pointers:
readlooks at every element once, left to right.writemarks the next slot of the answer. Everything beforewriteis finished.
Because the list is sorted, a value is new exactly when it differs from the last value you kept, which is numbers[write - 1]. When it is new, copy it to numbers[write] and advance write. When it is a repeat, just move on.
This is safe because write never gets ahead of read. The slot you overwrite has always been read already, so no unread value is ever lost.
Approach 2: read and write pointers
1def remove_duplicates(numbers: list[int]) -> int:2 """Keep one copy of each value at the front, in place; return how many."""3 if not numbers:4 return 05 write = 1 # numbers[0] is always kept6 for read in range(1, len(numbers)):7 if numbers[read] != numbers[write - 1]: # differs from last kept value8 numbers[write] = numbers[read]9 write += 110 return write # a count, not an indexDry run on [1, 1, 2, 2, 3]:
| read | numbers[read] | last kept numbers[write − 1] | New? | List after | write |
|---|---|---|---|---|---|
| start | — | — | — | [1, 1, 2, 2, 3] | 1 |
| 1 | 1 | 1 | no | [1, 1, 2, 2, 3] | 1 |
| 2 | 2 | 1 | yes | [1, 2, 2, 2, 3] | 2 |
| 3 | 2 | 2 | no | [1, 2, 2, 2, 3] | 2 |
| 4 | 3 | 2 | yes | [1, 2, 3, 2, 3] | 3 |
Returns 3, and numbers[0:3] is [1, 2, 3]. The tail [2, 3] is leftover, which the problem allows. On the second example, [3, 3, 3, 5, 8, 8], the list ends as [3, 5, 8, 5, 8, 8] and the function returns 3: the writer moved twice, once for 5 and once for 8, while the reader moved five times.
Why it is safe to overwrite. The writer only ever writes to slot write, and write ≤ read at every step — it starts at 1 like the reader, and it moves at most once per reader step. So the slot being written is either the one just read or one already read earlier. No value that still needs reading is ever destroyed. This is the sentence to say if the interviewer asks "are you sure you don't lose data?"
Complexity. read visits each element once; each visit does one comparison and at most one copy. O(n) time, O(1) space.
Edge cases
- Empty list. The guard returns 0 before
numbers[0]is touched. - One element. The loop does not run; returns 1.
- All equal, such as
[2, 2, 2]. Nothing is ever new; returns 1. - No duplicates. Every element is new; each one is copied onto itself and the function returns
n. - Negative values. Nothing changes; only equality and the sorted order matter.
We ran both approaches on 300 random sorted lists of up to 12 values, including empty lists and lists of one repeated value, and compared the kept prefix with sorted(set(numbers)) each time.
Follow-ups
- "Keep at most two copies of each value." Compare with the value two slots back in the output: keep
valueifwrite < 2orvalue != numbers[write - 2]. The same idea gives "at mostkcopies" withwrite - k. - "Move all zeroes to the end, keeping the order of the rest." Same reader and writer; on a non-zero, swap
numbers[write]andnumbers[read]and advancewrite. The zeroes collect behind the writer. - "The list is not sorted; keep the first copy of each value." Copies are no longer adjacent, so keep a
setof values seen:O(n)time,O(n)space. - "Remove every copy of one given value." This is the plain filter from the core-idea lesson: the writer moves whenever
numbers[read]is not that value. It does not even need sorted input, because the rule looks at one element at a time.
All of these share one design question, and it is worth asking out loud each time: what does the writer compare against? The last kept value for duplicates, the value two back for "keep two", a fixed value for "remove", zero for "move zeroes". Once that is fixed, the loop writes itself.