YeetCode
Data Structures & Algorithms · Arrays

Remove Duplicates from Sorted Array: The Two-Pointer In-Place Pattern

Solve Remove Duplicates from Sorted Array in-place with two pointers — intuition, exact JavaScript code, a worked walkthrough, complexity, and common mistakes.

7 min readBy Bhavesh Singh
arraystwo pointerstwo pointer in-placein-place mutationleetcode easy

This article has an interactive companion

Don't just read it — step through it interactively, one state change at a time.

Open the Remove Duplicates from Sorted Array visualizer

The array is already sorted, which means every duplicate is sitting right next to its twin. That single fact is the whole problem. You never have to search for repeats — you just walk forward and notice when a value changes.

The catch is the phrase in-place. You are not allowed to spin up a new array or a Set. You have to rearrange the unique values into the front of the array you were handed, using O(1) extra space, and return how many uniques there are. That constraint is what turns an easy dedupe into a clean two-pointer exercise.

The problem

Given a sorted integer array nums, remove the duplicates in-place so each value appears once, keeping the relative order. Return k, the number of unique elements. The first k slots of nums must hold those unique values; whatever sits past index k doesn't matter.

text
Input: nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4] Output: 5, nums = [0, 1, 2, 3, 4, _, _, _, _, _]

The underscores are values LeetCode's judge ignores. You return 5, and positions 0 through 4 must read 0, 1, 2, 3, 4. Two constraints shape everything:

  • Sorted input — equal values are always adjacent, so a duplicate is just a value equal to the one immediately before it.
  • In-place, O(1) space — no second array, so you overwrite the front of nums as you go.

The brute force baseline

If the in-place rule were relaxed, the obvious move is to dump everything into a Set and copy it back.

javascript
function removeDuplicates(nums) { const unique = [...new Set(nums)]; // O(n) extra space for (let i = 0; i < unique.length; i++) { nums[i] = unique[i]; } return unique.length; }

This is O(n) time, but it allocates a whole second structure to hold up to n values, so it's O(n) space — and it throws away the one thing you were given for free. The array is sorted. A Set treats the input as if the order were random, doing hash work to rediscover adjacency that was already handed to you. Interviewers ask this problem specifically to see whether you exploit the sorted precondition instead of ignoring it.

The key insight

In a sorted array, nums[read] is a new unique value exactly when it differs from nums[read - 1]. If they're equal, it's a repeat you've already recorded. That's the entire test — one comparison, no lookups, no memory.

So run two pointers across the same array:

  • read scans every element left to right, inspecting candidates.
  • write marks the next slot where a unique value belongs.

read always moves. write only moves when read lands on something new — and at that moment you copy nums[read] into nums[write]. Because write never outruns read, you always overwrite a slot you've already scanned, so nothing gets clobbered before it's read. This is the two-pointer in-place pattern: a slow write pointer trailing a fast read pointer, compacting good values toward the front.

The optimal solution

This is the exact algorithm the visualizer steps through, variable names and all.

javascript
var removeDuplicates = function (nums) { if (nums.length === 0) return 0; let write = 1; for (let read = 1; read < nums.length; read += 1) { if (nums[read] !== nums[read - 1]) { nums[write] = nums[read]; write += 1; } } return write; };

Three details carry the whole solution:

  • write starts at 1, not 0. Index 0 in a sorted array is always the first unique value — there's nothing before it to duplicate — so it's correct by default and the first free slot is index 1.
  • read starts at 1 too. The comparison nums[read] !== nums[read - 1] needs a valid read - 1, so scanning begins at index 1 and looks back at index 0.
  • The return value is write itself, not write + 1. Because write was seeded at 1 (already counting nums[0]), it ends as the exact count of uniques.

The comparison is against nums[read - 1], the previous read position, not nums[write - 1]. In a sorted array both give the same answer, and comparing to the untouched predecessor is the simplest way to reason about it.

Walkthrough

Trace nums = [1, 1, 2, 3, 3]. Watch write sit still on duplicates and step forward on new values. "nums after" shows the array once the step's write (if any) lands.

readnums[read]nums[read-1]New value?write (before → after)nums after
initwrite = 1[1, 1, 2, 3, 3]
111no1 → 1[1, 1, 2, 3, 3]
221yes1 → 2[1, 2, 2, 3, 3]
332yes2 → 3[1, 2, 3, 3, 3]
433no3 → 3[1, 2, 3, 3, 3]

The loop ends with write = 3, so the answer is 3, and the first three slots read [1, 2, 3] — exactly the uniques in order. The junk (3, 3) left in the tail is fine; the judge only inspects the first write elements.

Notice step read = 2: the value 2 gets copied into index 1, overwriting the second 1 that was already scanned and counted at read = 1. That's why compacting in-place is safe — write only ever lands on ground read has already covered.

Complexity

ApproachTimeSpaceWhy
Set + copy backO(n)O(n)allocates a second structure holding up to n values
Two-pointer in-placeO(n)O(1)one linear scan, only two integer pointers

The optimal version touches each element once with a single comparison and at most one assignment, so time is linear in the array length. Space is constant — write and read are the only extra variables, no matter how large nums grows.

Common mistakes

  • Returning write + 1. Because write is seeded at 1, it already counts the first element. Adding one over-reports the length by one. (If you instead start write = 0 and compare against nums[write], then you do return write + 1 — mixing the two conventions is the classic off-by-one here.)
  • Comparing nums[read] to nums[write] after mismatch handling gets tangled. Comparing to nums[read - 1] keeps the logic decoupled from where you're writing. Reach for that unless you have a reason not to.
  • Forgetting the empty-array guard. With nums.length === 0, seeding write = 1 and returning it would report 1 unique in an empty array. The early return 0 handles it.
  • Trying to actually delete elements. You never splice or shrink the array. You overwrite the front and return a count; the tail is intentionally left as-is.

Where this pattern shows up next

The slow-write / fast-read compaction shows up any time you filter or rearrange an array in-place:

  • Remove Element — the same write-pointer compaction, but keyed on "not equal to a target" instead of "not equal to the previous value".
  • Move Zeroes is a cousin idea, but for a closer match see Reverse String — two pointers converging from both ends instead of chasing.
  • Best Time to Buy and Sell Stock — one pass with a trailing "best so far" pointer, the same discipline of carrying only the state the next decision needs.
  • Merge Sorted Array — two read pointers plus a write pointer, walking from the back to avoid overwriting unread data.

You can also step through the algorithm interactively and watch the write pointer freeze on duplicates and jump forward on new values.

FAQ

Why does the write pointer start at 1 instead of 0?

In a sorted array, the element at index 0 is always the first occurrence of its value — there's nothing before it that it could duplicate. So it's already in the correct place and already counted. The first slot that might need overwriting is index 1, which is why both write and read begin there and the final write is the unique count directly.

What is the time and space complexity?

O(n) time and O(1) space. The single for loop visits each of the n elements once, doing one comparison and at most one assignment per element, so runtime scales linearly. The only extra memory is the two integer pointers write and read, which is constant regardless of input size — that's what satisfies the in-place requirement.

Does this approach work if the array is not sorted?

No. The whole method relies on duplicates being adjacent, which is only guaranteed when the array is sorted. On unsorted input, nums[read] !== nums[read - 1] would let through repeats that aren't neighbors. For unsorted data you'd need a Set (O(n) space) or a sort first (O(n log n) time) before this scan applies.

Why compare against nums[read - 1] instead of the last written value?

Comparing to nums[read - 1] keeps the duplicate test tied to the input's natural order rather than to wherever you happen to be writing. Since the array is sorted and read only moves forward, the previous read position is always the correct reference for "is this a new value?" Comparing against nums[write - 1] also works, but coupling the check to the write cursor makes the reasoning harder without buying anything.

Make it stick: run this one yourself

Don't just read it — step through it interactively, one state change at a time.

Open the Remove Duplicates from Sorted Array visualizer