Skip to content
← All posts

Remove Duplicates from Sorted Array: In-Place with Two Pointers

4 min read
leetcodeproblemeasyarraystwo-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.

00011213142526373849slowfast
Input array sorted in non-decreasing order. Slow marks the last unique position. Fast scans ahead looking for new values.

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.

0011122334slowfast

Duplicate found. fast advances, slow stays.

fast = 2: nums[fast] = 1 differs from nums[slow] = 0. Write it.

0111122334slowfast

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.

0111122334slowfast

Duplicate. fast advances.

fast = 4: nums[fast] = 1 equals nums[slow] = 1. Skip again.

0111122334slowfast

Still a duplicate. fast advances.

fast = 5: nums[fast] = 2 differs from nums[slow] = 1. Write it.

0121122334slowfast

New value. slow advances to 2, nums[2] = 2.

fast = 6: nums[fast] = 2 equals nums[slow] = 2. Skip.

0121122334slowfast

Duplicate. fast advances.

fast = 7: nums[fast] = 3 differs from nums[slow] = 2. Write it.

0123122334slowfast

New value. slow advances to 3, nums[3] = 3.

fast = 8: nums[fast] = 3 equals nums[slow] = 3. Skip.

0123122334slowfast

Duplicate. fast advances.

fast = 9: nums[fast] = 4 differs from nums[slow] = 3. Write it.

0123422334slowfast

New value. slow advances to 4, nums[4] = 4.

Done: fast has passed the end. Return slow + 1 = 5.

0123422334slowfast

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

MetricValue
TimeO(n), single pass through the array
SpaceO(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: fast never finds a different value, slow stays at 0, return 1.
  • All elements unique: every comparison triggers a write, slow keeps pace with fast, return len(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.