Remove Duplicates from Sorted Array: In-Place with Two Pointers
LeetCode Remove Duplicates from Sorted Array (problem 26) gives you a sorted integer array and asks you to remove duplicates in-place. You return k, the number of unique elements, and the first k slots of the array must hold those unique values in order. Whatever sits beyond index k - 1 does not matter.
The problem
Given nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4], produce k = 5 with nums[0..4] = [0, 1, 2, 3, 4]. A smaller example: nums = [1, 1, 2] produces k = 2 with nums[0..1] = [1, 2].
The array is already sorted in non-decreasing order. That single fact unlocks the entire solution, because duplicates are always adjacent.
Approach: slow/fast two pointers
Because the array is sorted, every group of duplicates sits in a contiguous run. You need two pointers:
- slow tracks the write position, pointing to the last confirmed unique element.
- fast scans ahead through the array looking for the next value that differs from
nums[slow].
When fast finds a value that is not equal to nums[slow], you advance slow by one and copy nums[fast] into that position. When fast sees a duplicate, it just moves forward. By the time fast reaches the end, every unique value has been written into nums[0..slow] in order.
Python solution
def remove_duplicates(nums: list[int]) -> int:
if not nums:
return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
Visual walkthrough
Trace through nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]. Watch how slow only advances when fast discovers a new value, and how the first part of the array accumulates the unique elements.
Start: slow = 0, fast = 1. nums[fast] = 0 equals nums[slow] = 0. Skip.
Duplicate found. fast advances, slow stays.
fast = 2: nums[fast] = 1 differs from nums[slow] = 0. Write it.
New value found. slow advances to 1, then nums[slow] = nums[fast]. Now nums[1] = 1.
fast = 3: nums[fast] = 1 equals nums[slow] = 1. Skip.
Duplicate. fast advances.
fast = 4: nums[fast] = 1 equals nums[slow] = 1. Skip again.
Still a duplicate. fast advances.
fast = 5: nums[fast] = 2 differs from nums[slow] = 1. Write it.
New value. slow advances to 2, nums[2] = 2.
fast = 6: nums[fast] = 2 equals nums[slow] = 2. Skip.
Duplicate. fast advances.
fast = 7: nums[fast] = 3 differs from nums[slow] = 2. Write it.
New value. slow advances to 3, nums[3] = 3.
fast = 8: nums[fast] = 3 equals nums[slow] = 3. Skip.
Duplicate. fast advances.
fast = 9: nums[fast] = 4 differs from nums[slow] = 3. Write it.
New value. slow advances to 4, nums[4] = 4.
Done: fast has passed the end. Return slow + 1 = 5.
The first 5 elements [0, 1, 2, 3, 4] are the unique values. Everything after index 4 does not matter.
Each time fast lands on a value different from nums[slow], that value gets written to the next available slot. Duplicates are simply skipped. By the end, the first five positions hold [0, 1, 2, 3, 4] and we return slow + 1 = 5.
Complexity
| Metric | Value |
|---|---|
| Time | O(n), single pass through the array |
| Space | O(1), no extra data structures |
The fast pointer visits every element exactly once. The slow pointer advances at most k times where k is the number of unique values. No allocations, no hash sets, no copying to a new array.
The building blocks
This problem decomposes into two reusable bricks:
Read-write two pointers
The core mechanic here is a read pointer (fast) scanning every element while a write pointer (slow) only moves when there is something worth writing. This same skeleton appears in many array-manipulation problems:
slow = 0
for fast in range(1, len(nums)):
if should_keep(nums[fast]):
slow += 1
nums[slow] = nums[fast]
Swap out should_keep for whatever predicate the problem needs and the structure stays identical.
In-place deduplication
The reason this works without extra space is the sorted order guarantee. Duplicates cluster together, so you only ever need to compare nums[fast] against nums[slow] (the most recent unique value). If the array were unsorted, you would need a hash set to track what you have already seen.
Edge cases
- Empty array: return 0 immediately. The guard clause handles this.
- All elements the same:
fastnever finds a different value,slowstays at 0, return 1. - All elements unique: every comparison triggers a write,
slowkeeps pace withfast, returnlen(nums). - Length 1: the loop body never executes since
range(1, 1)is empty, return 1.
The problem guarantees nums is sorted. If you are ever given an unsorted array, sort it first (O(n log n)) or use a set (O(n) space). The two-pointer approach only works when duplicates are adjacent.
From understanding to recall
This problem feels simple once you see the two-pointer idea. The risk is that "simple" makes you skip practice. Then in an interview, you fumble the off-by-one: should slow start at 0 or 1? Do you increment slow before or after writing? Do you return slow or slow + 1?
Those details lock in through repetition. Write the solution from scratch. Trace through [1, 1, 2] by hand. Do it again two days later. The goal is not to memorize the code but to internalize the pattern deeply enough that the code writes itself from the logic.
The read-write two-pointer pattern is one of the highest-leverage building blocks in array problems. Once you can write it without hesitation for deduplication, you will recognize it instantly in Move Zeroes (LeetCode 283), Remove Element (LeetCode 27), and anywhere else an array needs in-place compaction.
Related posts
- Sort Colors - A three-pointer extension of the read-write pattern, partitioning an array into three groups in a single pass.
- The Two Pointers Pattern - The foundational pattern behind slow/fast pointer techniques and converging pointer strategies.