Coding Interview Patterns

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.

After the pass over [1, 1, 2, 2, 3]1232301234write = 3read(last)The boxed prefix is the answer; slots 3 and 4 are leftovers the problem ignores.
Everything before the write pointer is finished output, and the writer never passes the reader.

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 k slots are checked. (In Python you could del 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.

Python
def remove_duplicates_brute(numbers: list[int]) -> int:    """Delete each repeat where it stands. Every pop shifts the tail left."""    i = 1    while i < len(numbers):        if numbers[i] == numbers[i - 1]:            numbers.pop(i)        else:            i += 1    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:

  • read looks at every element once, left to right.
  • write marks the next slot of the answer. Everything before write is 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

Python
def remove_duplicates(numbers: list[int]) -> int:    """Keep one copy of each value at the front, in place; return how many."""    if not numbers:        return 0    write = 1                            # numbers[0] is always kept    for read in range(1, len(numbers)):        if numbers[read] != numbers[write - 1]:    # differs from last kept value            numbers[write] = numbers[read]            write += 1    return write                         # a count, not an index

Dry run on [1, 1, 2, 2, 3]:

readnumbers[read]last kept numbers[write − 1]New?List afterwrite
start———[1, 1, 2, 2, 3]1
111no[1, 1, 2, 2, 3]1
221yes[1, 2, 2, 2, 3]2
322no[1, 2, 2, 2, 3]2
432yes[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 value if write < 2 or value != numbers[write - 2]. The same idea gives "at most k copies" with write - k.
  • "Move all zeroes to the end, keeping the order of the rest." Same reader and writer; on a non-zero, swap numbers[write] and numbers[read] and advance write. 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 set of 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.