Remove Duplicates from Sorted Array — Write Pointer Pattern Explained [Easy]

Sanjeev SharmaSanjeev Sharma
12 min read

Advertisement

Problem Statement

Given an integer array nums sorted in non-decreasing order, remove the duplicates in-place so that each unique element appears only once. Return k — the number of unique elements. The first k elements of nums must contain the unique elements in their original order.

Examples:

Input:  nums = [1, 1, 2]
Output: k = 2, nums = [1, 2, _]

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

The underscore _ means the judge doesn't care what values are stored past index k-1.

Why This Problem Matters

LeetCode 26 is not just an easy warm-up — it teaches the write pointer pattern, which is the canonical technique for in-place array modification. Every time an interview asks you to filter, compress, or deduplicate an array without extra space, this is the blueprint you reach for.

The pattern shows up across a surprisingly wide range of problems:

  • LC 27 — Remove Element (filter out a specific value)
  • LC 80 — Remove Duplicates II (allow up to 2 occurrences)
  • LC 283 — Move Zeroes (partition values to the end)
  • Any problem with the shape: "keep elements that satisfy condition X, discard the rest, in-place"

Understanding the write pointer deeply means you can solve all of these variants with minimal mental overhead. The sorted array in LC 26 is what makes this version the cleanest starting point: the structure of the input reduces the duplicate detection to a single comparison, making the pattern easy to see clearly before you apply it to harder problems.

The Write Pointer Pattern

The core idea is to maintain two roles within the same array:

  • The read role — an index that walks through every element in the array, left to right, no skipping.
  • The write role — an index that only advances when we decide to keep the current element.

At the end, the write index equals k, the count of valid elements, because it advanced exactly once for every unique element we decided to keep.

Here is the algorithm in pseudocode:

write = 1   # index 0 is always unique — there's nothing before it to duplicate
for i = 1 to len(nums) - 1:
    if nums[i] != nums[write - 1]:   # found a new unique value
        nums[write] = nums[i]         # place it at the write position
        write += 1                    # advance write for the next slot
return write

A few things to notice:

Why does write start at 1? The element at index 0 is always the first (and therefore unique) element. There is nothing before it to compare against, so we start with write = 1 already "accepting" index 0 and prepare to place the next unique element at position 1.

Why compare with nums[write - 1] and not nums[i - 1]? This is the most misunderstood part of the pattern. nums[write - 1] is the last element we actually kept. nums[i - 1] might be a duplicate that we just skipped over. Comparing with the wrong value leads to incorrectly keeping duplicates or missing unique elements. Always look back at what you wrote, not at what the read pointer just passed.

Why does this work without extra space? Because the write index is always less than or equal to the read index — we never overwrite an element before we have had a chance to read it.

Why a Sorted Array Simplifies Everything

In an unsorted array, detecting duplicates requires a hash set: you have to remember every element you have ever seen because a duplicate might appear anywhere, not just nearby.

In a sorted array, all duplicates are contiguous. The value 1 cannot appear at index 2 and again at index 9 without every element between them also being 1. This means the only duplicate you ever need to worry about is the one immediately behind the write pointer. A single comparison, nums[i] != nums[write - 1], is sufficient to determine uniqueness.

This is also the key difference between LC 26 and LC 283 (Move Zeroes). In Move Zeroes, the input is unsorted, but the condition is simple (is this element zero?). In LC 26, the condition is relational (is this element a duplicate of the last kept value?), but the sorted order makes that relation cheap to evaluate. Both use the write pointer; the condition that gates the write is what changes.

Visual Dry Run

Let's trace nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4] step by step.

write = 1 initially. We begin reading from i = 1.

Step 1: i=1  nums[i]=0  nums[write-1]=nums[0]=0EQUAL, skip
  array: [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]   write=1

Step 2: i=2  nums[i]=1  nums[write-1]=nums[0]=0DIFFERENT, write
  array: [0, 1, 1, 1, 1, 2, 2, 3, 3, 4]   write=2

Step 3: i=3  nums[i]=1  nums[write-1]=nums[1]=1EQUAL, skip
  array: [0, 1, 1, 1, 1, 2, 2, 3, 3, 4]   write=2

Step 4: i=4  nums[i]=1  nums[write-1]=nums[1]=1EQUAL, skip
  array: [0, 1, 1, 1, 1, 2, 2, 3, 3, 4]   write=2

Step 5: i=5  nums[i]=2  nums[write-1]=nums[1]=1DIFFERENT, write
  array: [0, 1, 2, 1, 1, 2, 2, 3, 3, 4]   write=3

Step 6: i=6  nums[i]=2  nums[write-1]=nums[2]=2EQUAL, skip
  array: [0, 1, 2, 1, 1, 2, 2, 3, 3, 4]   write=3

Step 7: i=7  nums[i]=3  nums[write-1]=nums[2]=2DIFFERENT, write
  array: [0, 1, 2, 3, 1, 2, 2, 3, 3, 4]   write=4

Step 8: i=8  nums[i]=3  nums[write-1]=nums[3]=3EQUAL, skip
  array: [0, 1, 2, 3, 1, 2, 2, 3, 3, 4]   write=4

Step 9: i=9  nums[i]=4  nums[write-1]=nums[3]=3DIFFERENT, write
  array: [0, 1, 2, 3, 4, 2, 2, 3, 3, 4]   write=5

Return: k = 5
First k elements: [0, 1, 2, 3, 4]

Notice how the write pointer lags behind the read pointer. The "garbage" values in positions 5–9 do not matter — the judge only checks the first k elements.

Common Mistakes

Mistake 1: Starting write at 0 instead of 1

If you initialize write = 0 and begin your loop at i = 0, you end up comparing nums[0] with nums[-1] (the last element in most languages), which is almost certainly wrong. Worse, if you compare nums[i] with nums[write] rather than nums[write - 1], you overwrite nums[0] with itself before you have even read anything. The first element is always unique by definition — begin your loop at i = 1 with write = 1.

Mistake 2: Comparing nums[i] with nums[i - 1] instead of nums[write - 1]

This is the classic confusion. When the read pointer has jumped ahead, nums[i - 1] might be a duplicate that was correctly skipped. Comparing with it can cause you to write a value that is actually a duplicate of nums[write - 1]. Always compare against the last value you committed to the output section of the array, which is nums[write - 1].

Mistake 3: Returning write - 1 or nums.length instead of write

The variable write ends up equaling the count of unique elements because it advances exactly once per kept element. It is already k. Off-by-one errors here — returning write + 1 or write - 1 — will cause the judge to either check one too few or one too many elements. Return write as-is.

The LC 80 Generalization — Allow Up to 2 Occurrences

LeetCode 80 asks the same question but allows each element to appear at most twice. The write pointer pattern generalizes elegantly.

For LC 26 (at most 1 occurrence), the condition to keep an element is:

nums[i] != nums[write - 1]

"The current element must differ from the most recent kept element."

For LC 80 (at most 2 occurrences), the condition becomes:

nums[i] != nums[write - 2]

"The current element must differ from the element that was kept two slots ago."

Why does this work? If nums[write - 2] equals the current element, then positions write-2 and write-1 already hold that value — adding one more would create a third occurrence. If they differ, we have at most one prior occurrence and can safely write.

The general pattern for allowing up to k occurrences:

nums[i] != nums[write - k]

This single-line change is all that separates LC 26 from LC 80. That elegance is the hallmark of a well-understood pattern.

Solutions

Python — LC 26: Remove Duplicates from Sorted Array

from typing import List

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        # Edge case: empty array has 0 unique elements
        if not nums:
            return 0

        # write starts at 1 because nums[0] is always unique
        write = 1

        for i in range(1, len(nums)):
            # Compare current element with the last element we kept
            if nums[i] != nums[write - 1]:
                # Found a new unique value — place it at the write position
                nums[write] = nums[i]
                write += 1  # advance write to the next open slot

        # write == the count of unique elements
        return write

JavaScript — LC 26: Remove Duplicates from Sorted Array

/**
 * @param {number[]} nums
 * @return {number}
 */
function removeDuplicates(nums) {
    if (nums.length === 0) return 0;

    // write starts at 1; index 0 is always a unique element
    let write = 1;

    for (let i = 1; i < nums.length; i++) {
        // Only write when we find a value different from the last kept element
        if (nums[i] !== nums[write - 1]) {
            nums[write] = nums[i]; // copy unique element to write position
            write++;               // advance write for the next slot
        }
    }

    // write is the count k of unique elements
    return write;
}

Python — LC 80: Remove Duplicates II (at most 2 occurrences)

from typing import List

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        # write starts at 2 because the first two elements are always valid:
        # even if nums[0] == nums[1], that's still within the "at most 2" limit.
        write = 2

        if len(nums) <= 2:
            return len(nums)

        for i in range(2, len(nums)):
            # Keep if current differs from the element two slots behind write.
            # If nums[write-2] == nums[i], positions write-2 and write-1
            # already hold this value twice — a third copy would exceed the limit.
            if nums[i] != nums[write - 2]:
                nums[write] = nums[i]
                write += 1

        return write

JavaScript — LC 80: Remove Duplicates II (at most 2 occurrences)

/**
 * @param {number[]} nums
 * @return {number}
 */
function removeDuplicates(nums) {
    if (nums.length <= 2) return nums.length;

    // First two elements always satisfy the "at most 2" constraint
    let write = 2;

    for (let i = 2; i < nums.length; i++) {
        // If the element two positions behind write matches current,
        // we already have 2 copies — skip this one
        if (nums[i] !== nums[write - 2]) {
            nums[write] = nums[i];
            write++;
        }
    }

    return write;
}

Complexity Analysis

SolutionTimeSpaceNotes
LC 26O(n)O(1)Single pass, two index variables
LC 80O(n)O(1)Single pass, same structure

Both algorithms make exactly one pass through the array. The write pointer can never advance faster than the read pointer (write <= i always holds), so neither solution allocates any data structure proportional to the input size.

Follow-up Questions

LC 27 — Remove Element: Given nums and a value val, remove all occurrences of val in-place and return the count of remaining elements. The write pointer condition becomes nums[i] != val — keep anything that is not the target. write starts at 0 here because even the first element might need to be removed.

LC 80 — Remove Duplicates from Sorted Array II: Covered above. Change nums[write - 1] to nums[write - 2] and start write at 2.

Allow at most k duplicates (general): Start write at k. The condition becomes nums[i] != nums[write - k]. This unified formula handles LC 26 (k=1) and LC 80 (k=2) as special cases.

Unsorted input: If the array were unsorted, you would need a hash set to track seen values — O(n) space. The sorted property is what collapses the problem to O(1) space.

This Pattern Solves

The write pointer generalizes to any problem with the structure "scan a sequence, keep elements satisfying condition C, discard the rest, in-place":

  • LC 27 — Remove Element: Condition is nums[i] != val
  • LC 283 — Move Zeroes: Condition is nums[i] != 0 (then fill remainder with zeros)
  • LC 26 — Remove Duplicates I: Condition is nums[i] != nums[write - 1]
  • LC 80 — Remove Duplicates II: Condition is nums[i] != nums[write - 2]
  • LC 905 — Sort Array By Parity: Two-pass or one-pass write pointer with odd/even condition
  • String filtering problems: Remove characters matching a pattern in a char array

Each time, the template is the same: initialize write, scan with i, check condition, write-and-advance or skip.

Key Takeaway

The write pointer pattern is deceptively simple — a single index and a single condition — but it is the backbone of in-place array modification. The sorted array in LC 26 earns its keep by making duplicate detection a one-comparison affair: because duplicates are contiguous, you only ever need to look at nums[write - 1], the last element you committed. Internalize the comparison direction (write - 1, not i - 1), the initialization (write = 1), and the return value (write, not write - 1), and you have a template that scales from LC 26 all the way to the allow-k-duplicates generalization with a single variable change.

Problem 7 — Merge Sorted Array | Problem 9 — Single Number

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro