Remove duplicates from a sorted array in place

Two pointers moving the same way. Read scans every element; write marks the end of the kept prefix. When read finds something new, copy it back to write and advance. O(n) time, O(1) extra space — and the function returns a length, because nothing was reallocated.

Overview

The problem

Given a sorted array, remove duplicates in place and return the new length. The elements beyond that length do not matter.

[1, 1, 2, 2, 3] → length 3, array starts [1, 2, 3, ...]

Two constraints define it. In place means O(1) extra space — no new array. Sorted means duplicates are adjacent, which is what makes a single pass possible.

The sortedness is doing the work: on unsorted input, adjacency tells you nothing and a set (O(n) space) or a sort (O(n log n)) is needed.

Lists & arraysCoding problemEasy

Step through it

What to watch

  • write only advances on a genuinely new value.
  • The comparison is against the last kept element, not the previous one read.
  • The tail is left as stale data — that is why a length is returned.

Say this out loud

"Fast and slow pointers. Read scans, write marks the end of the deduped prefix, and I copy back only when the value differs from the last kept one. O(n) time, O(1) space, and I return the length because the tail is stale."

Remove duplicates from a sorted array in place

Remove duplicates from a sorted array in place and return the new length.

Run it

The two-pointer version with the array printed after each write, the same pattern applied to two sibling problems, and the off-by-one comparison that looks right and is not.

1Python
Output
2Python
Output
3Python
Output
4Python
Output

Two pointers, read and write

5Python
Output

O(n) time, O(1) space.

The invariant is what makes it correct: everything before write is the deduplicated result so far. Naming that invariant out loud is worth more than the code, because it is what lets you verify the comparison is right.

The comparison is against nums[write - 1] — the last value kept — not against nums[read - 1], the previous value read. On sorted input those coincide, and using the kept value generalises correctly to the variants below.

Tracing it

[1, 1, 2, 2, 3]:

readnums[read]nums[write−1]ActionArraywrite
111Skip — duplicate[1,1,2,2,3]1
221Write at 1[1,2,2,2,3]2
322Skip[1,2,2,2,3]2
432Write at 2[1,2,3,2,3]3

Returns 3, and nums[:3] is [1, 2, 3]. The tail [2, 3] is leftover and explicitly allowed to be anything.

That leftover is worth pointing out, because the natural expectation is that the array is fully cleaned. The contract is only about the first write elements.

Why sorted matters

Duplicates in a sorted array are adjacent, so "have I seen this before?" collapses to "is it the same as the last one I kept?" — one comparison, no set, no memory.

On unsorted input this does not work and you need a set, which costs O(n) space, or a sort first, which costs O(n log n) time. Say which assumption you are relying on.

The read/write pattern

read visits every element exactly once. write lags behind, marking where the next kept element goes. Because write never overtakes read, the copy can never clobber something not yet examined — which is what makes in-place safe.

Compare against values[write - 1], the last element actually kept, not values[read - 1]. On a long run of duplicates those differ, and the second is wrong.

Why it returns a length

Nothing is reallocated, so the array keeps its original size and everything past write is leftover data. The caller uses values[:n]. This is the C-style convention the question comes from, and returning a new list instead would defeat the O(1) space requirement the question exists to test.

The same pattern solves "move zeroes to the end", "remove all instances of a value", and "allow at most two duplicates" — only the keep condition changes.

The variants

Allow up to k duplicates. The generalisation, and it is elegant — compare against the element k positions back:

6Python
Output

With k = 1 this is the original; with k = 2 it allows each value twice. The write < k guard handles the start, and comparing against nums[write - k] asks "have I already kept k copies of this value?"

Remove all occurrences of a specific value:

7Python
Output

Same shape, different condition. Recognising that these are one pattern with a varying predicate is the useful observation.

Move zeros to the end is the same again: write the non-zeros forward, then fill the remainder with zeros.

Unsorted input. Requires a set for O(n) time and O(n) space, or sorting first for O(1) space at O(n log n). State the trade rather than pretending in-place O(n) is available.

Edge cases

CaseResult
[]0
[1]1
All identical, [2,2,2]1
All distinctUnchanged length
Two elements equal1
Not actually sortedWrong answer, silently

The last row is the important one. The algorithm relies on duplicates being adjacent, and on unsorted input it removes only adjacent repeats and returns a length that is too large. It fails without any error, which is worse than raising.

If sortedness is not guaranteed, say so and ask — do not assume.

Why the write-pointer pattern generalises

This is one instance of a broader technique: a read pointer scanning and a write pointer compacting, used whenever an array must be filtered in place.

ProblemWrite condition
Remove duplicates (sorted)Differs from the last kept
Remove a valueNot equal to the value
Move zeros to the endNon-zero
Keep at most k duplicatesDiffers from k positions back
Partition by a predicatePredicate holds
Dutch national flagThree regions, three pointers

All of them are O(n) time and O(1) space, and all of them are the same loop with a different if. Recognising the family means you write it correctly the first time rather than re-deriving it.

The corresponding out-of-place version is a list comprehension — [x for x in nums if x != val] — which is clearer and uses O(n) space. Choose in-place only when the constraint requires it.

What it is testing

Do you use two pointers rather than deleting from the list? nums.remove(x) or del nums[i] inside a loop is O(n) per operation and shifts the elements you are iterating over — a double mistake.

Do you state the invariant? "Everything before write is the answer so far" is what makes the solution verifiable.

Do you compare against the last kept element, not the previous read one?

Do you notice that sortedness is required? And ask if it is not stated.

Do you know the tail is allowed to be garbage? The contract is the returned length, not the whole array.

Recap in one screen

  • Sorted input means duplicates are adjacent, which allows one pass with O(1) space.
  • A read pointer scans; a write pointer marks where the next kept element goes.
  • Compare against the last kept element, nums[write - 1], not the previous read one.
  • Elements past the returned length are undefined — that is part of the contract.
  • The read/write compaction pattern covers remove-value, move-zeros and keep-at-most-k with a different condition.

How the code works

The two-pointer version with the array printed after each write, the same pattern applied to two sibling problems, and the off-by-one comparison that looks right and is not.

How the code works

  1. values[read] != values[write - 1]Compare with the last element actually kept. Using values[read - 1] instead happens to agree on sorted input and is the wrong invariant to carry into the variants.
  2. write += 1 only on a keepwrite is the length of the answer so far, which is why returning it at the end needs no separate counter.
  3. write never overtakes readThe safety argument for writing into the array you are reading. Every slot written has already been consumed.
  4. if write < 2 or v != values[write - 2]:The at-most-two variant. Same skeleton, one different condition — which is what makes this pattern worth recognising rather than memorising.

Change one thing

  • Print the whole array rather than values[:n]. The stale tail is exactly why the function cannot just return the array.
  • Adapt it to remove every instance of a given value. One condition changes and nothing else does.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Why does the function return a length rather than a list?

  2. The current element is compared against:

  3. Writing into the array while reading it is safe because:

Cheat sheet

Remove duplicates from a sorted array in place

Two pointers moving the same way. Read scans every element; write marks the end of the kept prefix. When read finds something new, copy it back to write and advance. O(n) time, O(1) extra space — and the function returns a length, because nothing was reallocated.

INTERVIEW · vizlearn.in/interview/remove-duplicates-in-place.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.