Remove Duplicates from Sorted Array — Write Pointer Pattern Explained [Easy]
Advertisement
Problem Statement
Given an integer array
numssorted in non-decreasing order, remove the duplicates in-place so that each unique element appears only once. Returnk— the number of unique elements. The firstkelements ofnumsmust 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
- The Write Pointer Pattern
- Why a Sorted Array Simplifies Everything
- Visual Dry Run
- Common Mistakes
- The LC 80 Generalization — Allow Up to 2 Occurrences
- Solutions
- Python — LC 26: Remove Duplicates from Sorted Array
- JavaScript — LC 26: Remove Duplicates from Sorted Array
- Python — LC 80: Remove Duplicates II (at most 2 occurrences)
- JavaScript — LC 80: Remove Duplicates II (at most 2 occurrences)
- Complexity Analysis
- Follow-up Questions
- This Pattern Solves
- Key Takeaway
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]=0 → EQUAL, 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]=0 → DIFFERENT, 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]=1 → EQUAL, 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]=1 → EQUAL, 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]=1 → DIFFERENT, 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]=2 → EQUAL, 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]=2 → DIFFERENT, 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]=3 → EQUAL, 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]=3 → DIFFERENT, 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
| Solution | Time | Space | Notes |
|---|---|---|---|
| LC 26 | O(n) | O(1) | Single pass, two index variables |
| LC 80 | O(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