Remove Duplicates from Sorted Array (LeetCode 26)
You're given an already-sorted (smallest to largest) integer array nums. Rewrite it in-place so every value appears just once, keeping the surviving values in their original relative order.
Say k is the count of distinct values in nums. Once the duplicates are gone, hand back that count k, and make sure the first k slots of nums hold those distinct values in sorted order — whatever sits past index k - 1 is never inspected.
Custom Judge:
We compare the k you return with the length of the expected trimmed array, then check that each of the first k slots of nums holds the expected value for that position. Anything after slot k - 1 is ignored.
Worked Examples
nums = [1,1,2]2, nums = [1,2,_]nums = [0,0,1,1,1,2,2,3,3,4]5, nums = [0,1,2,3,4,_,_,_,_,_]⚖️Formal Constraints & Bounds
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 100numsis sorted in non-decreasing order.
Core Insight
Sortedness ensures duplicate elements are strictly adjacent, enabling single-pass linear filtering in O(1) space.
Real-World Scenario & Production Applications
Memory compaction algorithms, columnar database run-length deduplication, and zero-allocation stream filters.
Subproblems & Recurrence Decomposition3 Phases
slow tracks the next write position for a unique value; fast scans ahead reading every element. Since the array is sorted, duplicates are always adjacent.
slow = 1 # first element is trivially uniqueStep-by-Step Execution Trace Table
| Step | reader | nums[reader] | nums[writer-1] | Action | Mutated Array Prefix | writer |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | Duplicate (0 == 0) → skip | [0, ...] | 1 |
| 2 | 2 | 1 | 0 | Unique (1 != 0) → nums[1]=1, writer++ | [0, 1, ...] | 2 |
| 3 | 3 | 1 | 1 | Duplicate (1 == 1) → skip | [0, 1, ...] | 2 |
| 4 | 4 | 1 | 1 | Duplicate (1 == 1) → skip | [0, 1, ...] | 2 |
| 5 | 5 | 2 | 1 | Unique (2 != 1) → nums[2]=2, writer++ | [0, 1, 2, ...] | 3 |
| 6 | 6 | 2 | 2 | Duplicate (2 == 2) → skip | [0, 1, 2, ...] | 3 |
| 7 | 7 | 3 | 2 | Unique (3 != 2) → nums[3]=3, writer++ | [0, 1, 2, 3, ...] | 4 |
| 8 | 8 | 3 | 3 | Duplicate (3 == 3) → skip | [0, 1, 2, 3, ...] | 4 |
| 9 | 9 | 4 | 3 | Unique (4 != 3) → nums[4]=4, writer++ | [0, 1, 2, 3, 4] | 5 |
| 1 | if not nums: |
| 2 | return 0 |
| 3 | writer = 1 |
| 4 | for reader in range(1, len(nums)): |
| 5 | if nums[reader] != nums[writer - 1]: |
| 6 | nums[writer] = nums[reader] |
| 7 | writer += 1 |
| 8 | return writer |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Two pointers moving through the same array, one always at or behind the other, is the foundational shape of this entire pattern — worth slowing down on before it becomes muscle memory.
🧹 The Analogy: A Librarian Re-Shelving Duplicates
Picture a librarian walking down a single shelf of books already sorted alphabetically, holding a "next open slot" marker (writer) in one hand. They scan forward with their eyes (reader), one book at a time. Whenever they spot a book different from the last one they kept, they physically slide it back into the open slot and advance the marker. Duplicate books get scanned past and simply never get a slot — they're silently overwritten by whatever comes after them.
🔑 The Key Idea in the Code
Because the shelf is already sorted, every duplicate of a book sits immediately next to it — so "is this different from the last book I kept?" is a complete duplicate check, with no need to remember anything beyond the single most recent kept value.
🤔 Common Point of Confusion: Why writer - 1 Instead of reader - 1?
Comprehending the comparison on the first iteration can feel unnatural: we are comparing index 1 with index 0, and writer - 1 forces you to juggle two moving boundaries at once.
Since the array is sorted, duplicates are always adjacent, so you could also write:
# The "Compare with Previous" Approach:if nums[reader] != nums[reader - 1]: nums[writer] = nums[reader] writer += 1 Why this feels intuitive:
- Separation of Concerns: The
readeris solely responsible for discovering new unique elements by checking the card it just held (reader - 1). Thewriterstrictly acts as the "next open slot" marker. - Mental Model: Like flipping through a deck of sorted cards, you just ask: "Is this card different from the card in my hand one second ago?"
🏛️ Architectural Insight: Which Version Is Canonically "Correct" and Why?
While nums[reader] != nums[reader - 1] is convenient for LC 26, nums[reader] != nums[writer - 1] is the canonically correct, architecturally superior version for the general pattern:
-
Trust the "Kept" Data, Not the "Raw" Data: The core invariant of compaction is that the prefix
nums[0..writer-1]represents your finalized, verified, processed data (the "Kept-Prefix Boundary"). The right side (nums[reader..]) is untrusted, raw input. When deciding whether to keep an element, you should compare against the last element you actually decided to keep (writer - 1), not just whatever you looked at a moment ago. -
Generalizability to Harder Problems (The Main Advantage): Comparing against
writerallows you to solve advanced variants with almost zero code changes. Consider LC 80 (Remove Duplicates II), where you can keep at most 2 duplicates (or generally at most duplicates).
- With
reader - 1, you need messy state flags and counters to track consecutive duplicate counts. - With the
writerpattern, the code remains identical—you simply look slots back in your trusted data:
def removeDuplicatesTwice(nums: list[int]) -> int: if len(nums) <= 2: return len(nums) writer = 2 for reader in range(2, len(nums)): # Is this number different from what I kept TWO slots ago? if nums[reader] != nums[writer - 2]: nums[writer] = nums[reader] writer += 1 return writer Prematurely advancing writer before writing: The writer index marks the boundary where the next unique element belongs. Overwriting
nums[writer] = nums[reader]before incrementingwriter += 1is required, or else you leave gaps.Comparing
readeragainstreader - 1instead of trustedwriter - 1: In standard deduplication, comparing withreader - 1works for single duplicates, but fails to generalize to -duplicates. The canonical boundary-of-truth compares withwriter - 1.Returning
len(nums)instead ofwriter: The array is compacted in-place to indexwriter - 1; the total count of unique preserved elements iswriter, not the original array length.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Sorted array + in-place modification requirement + O(1) extra memory constraint.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: nums[0..writer-1] contains the sorted unique elements. If nums[reader] != nums[writer-1], nums[writer] = nums[reader] and writer increments.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Comparing against
nums[writer](the next free slot) instead ofnums[writer - 1](the last kept value): on[0,0,1,1,1,2,2,3,3,4]it keeps 7 values instead of 5.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use Read-Write Pointers. The array is sorted, so equal values sit next to each other, and a value is new exactly when it differs from the last value I kept. writer marks the next free slot and starts at 1, because nums[0] is always kept; reader walks from index 1 to the end. When nums[reader] differs from nums[writer - 1], I copy it to nums[writer] and move writer forward. writer never passes reader, so I only ever overwrite a slot I have already read, and nums[0] to nums[writer - 1] always holds the distinct values in order. The trap is comparing with nums[writer] instead of nums[writer - 1]: that slot is the next free one and still holds an old value, so on [1, 2, 2] the second 2 is lost and I return 1 instead of 2. It's
O(N)time andO(1)extra space, and I return writer."
Complexity & Mathematical Proof
O(N)
Look at the loop: for reader in range(1, len(nums)) visits each element once and does one comparison with nums[writer - 1]; writer only moves when a new value is copied, so each slot is written at most once. N - 1 iterations of O(1) work: O(N).
O(1) Auxiliary
Look at memory allocations: Call stack uses O(1) Iterative. In-place array manipulation overwriting indices in nums using two integer indices.
T(N) = ∑_{j=1}^N O(1) = O(N)
Derivation Progression
for reader ∈ [1, N-1]
reader visits every element after the first exactly once.
if nums[reader] != nums[writer - 1]: nums[writer] = nums[reader]; writer += 1
writer advances only when a new value is copied to the next free slot.
O(N) total steps
Array is partitioned in-place in exactly N iterations.
Variable Definitions
Total number of elements in the sorted array
Memory Architecture & Bounds
O(1) Iterative
O(1) Zero heap memory
O(1) Returns integer count
Boundary Best / Worst Cases
Pointer Invariant Transition Progression
Senior SWE Deconstruction & Hardware Caveats
Triggers: "remove duplicates in-place from sorted array", "return number of unique elements k", "do not allocate extra space". The sorted property guarantees duplicates are strictly adjacent. Reader-writer pointer compaction pattern partitions array into [0..write-1] (unique elements) and [write..N-1] (garbage/unprocessed).
. Requires strictly time and auxiliary space. Boundary choice: write pointer initializes to index 1 because the first element is unconditionally unique.
base case (loop does not execute, returns 1 correctly). Overwriting candidate elements before inspection (avoided by write <= read). Comparing candidate with nums[read-1] instead of nums[write-1] (both work when sorted, but write-1 generalizes to k-duplicate compaction).
Remove Duplicates from Sorted Array (LeetCode 26)
You're given an already-sorted (smallest to largest) integer array nums. Rewrite it in-place so every value appears just once, keeping the surviving values in their original relative order.
Say k is the count of distinct values in nums. Once the duplicates are gone, hand back that count k, and make sure the first k slots of nums hold those distinct values in sorted order — whatever sits past index k - 1 is never inspected.
Custom Judge:
We compare the k you return with the length of the expected trimmed array, then check that each of the first k slots of nums holds the expected value for that position. Anything after slot k - 1 is ignored.
Worked Examples
nums = [1,1,2]2, nums = [1,2,_]nums = [0,0,1,1,1,2,2,3,3,4]5, nums = [0,1,2,3,4,_,_,_,_,_]⚖️Formal Constraints & Bounds
1 <= nums.length <= 3 * 104-100 <= nums[i] <= 100numsis sorted in non-decreasing order.
Core Insight
Sortedness ensures duplicate elements are strictly adjacent, enabling single-pass linear filtering in O(1) space.
Real-World Scenario & Production Applications
Memory compaction algorithms, columnar database run-length deduplication, and zero-allocation stream filters.
Subproblems & Recurrence Decomposition3 Phases
slow tracks the next write position for a unique value; fast scans ahead reading every element. Since the array is sorted, duplicates are always adjacent.
slow = 1 # first element is trivially uniqueStep-by-Step Execution Trace Table
| Step | reader | nums[reader] | nums[writer-1] | Action | Mutated Array Prefix | writer |
|---|---|---|---|---|---|---|
| 1 | 1 | 0 | 0 | Duplicate (0 == 0) → skip | [0, ...] | 1 |
| 2 | 2 | 1 | 0 | Unique (1 != 0) → nums[1]=1, writer++ | [0, 1, ...] | 2 |
| 3 | 3 | 1 | 1 | Duplicate (1 == 1) → skip | [0, 1, ...] | 2 |
| 4 | 4 | 1 | 1 | Duplicate (1 == 1) → skip | [0, 1, ...] | 2 |
| 5 | 5 | 2 | 1 | Unique (2 != 1) → nums[2]=2, writer++ | [0, 1, 2, ...] | 3 |
| 6 | 6 | 2 | 2 | Duplicate (2 == 2) → skip | [0, 1, 2, ...] | 3 |
| 7 | 7 | 3 | 2 | Unique (3 != 2) → nums[3]=3, writer++ | [0, 1, 2, 3, ...] | 4 |
| 8 | 8 | 3 | 3 | Duplicate (3 == 3) → skip | [0, 1, 2, 3, ...] | 4 |
| 9 | 9 | 4 | 3 | Unique (4 != 3) → nums[4]=4, writer++ | [0, 1, 2, 3, 4] | 5 |
| 1 | if not nums: |
| 2 | return 0 |
| 3 | writer = 1 |
| 4 | for reader in range(1, len(nums)): |
| 5 | if nums[reader] != nums[writer - 1]: |
| 6 | nums[writer] = nums[reader] |
| 7 | writer += 1 |
| 8 | return writer |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Two pointers moving through the same array, one always at or behind the other, is the foundational shape of this entire pattern — worth slowing down on before it becomes muscle memory.
🧹 The Analogy: A Librarian Re-Shelving Duplicates
Picture a librarian walking down a single shelf of books already sorted alphabetically, holding a "next open slot" marker (writer) in one hand. They scan forward with their eyes (reader), one book at a time. Whenever they spot a book different from the last one they kept, they physically slide it back into the open slot and advance the marker. Duplicate books get scanned past and simply never get a slot — they're silently overwritten by whatever comes after them.
🔑 The Key Idea in the Code
Because the shelf is already sorted, every duplicate of a book sits immediately next to it — so "is this different from the last book I kept?" is a complete duplicate check, with no need to remember anything beyond the single most recent kept value.
🤔 Common Point of Confusion: Why writer - 1 Instead of reader - 1?
Comprehending the comparison on the first iteration can feel unnatural: we are comparing index 1 with index 0, and writer - 1 forces you to juggle two moving boundaries at once.
Since the array is sorted, duplicates are always adjacent, so you could also write:
# The "Compare with Previous" Approach:if nums[reader] != nums[reader - 1]: nums[writer] = nums[reader] writer += 1 Why this feels intuitive:
- Separation of Concerns: The
readeris solely responsible for discovering new unique elements by checking the card it just held (reader - 1). Thewriterstrictly acts as the "next open slot" marker. - Mental Model: Like flipping through a deck of sorted cards, you just ask: "Is this card different from the card in my hand one second ago?"
🏛️ Architectural Insight: Which Version Is Canonically "Correct" and Why?
While nums[reader] != nums[reader - 1] is convenient for LC 26, nums[reader] != nums[writer - 1] is the canonically correct, architecturally superior version for the general pattern:
-
Trust the "Kept" Data, Not the "Raw" Data: The core invariant of compaction is that the prefix
nums[0..writer-1]represents your finalized, verified, processed data (the "Kept-Prefix Boundary"). The right side (nums[reader..]) is untrusted, raw input. When deciding whether to keep an element, you should compare against the last element you actually decided to keep (writer - 1), not just whatever you looked at a moment ago. -
Generalizability to Harder Problems (The Main Advantage): Comparing against
writerallows you to solve advanced variants with almost zero code changes. Consider LC 80 (Remove Duplicates II), where you can keep at most 2 duplicates (or generally at most duplicates).
- With
reader - 1, you need messy state flags and counters to track consecutive duplicate counts. - With the
writerpattern, the code remains identical—you simply look slots back in your trusted data:
def removeDuplicatesTwice(nums: list[int]) -> int: if len(nums) <= 2: return len(nums) writer = 2 for reader in range(2, len(nums)): # Is this number different from what I kept TWO slots ago? if nums[reader] != nums[writer - 2]: nums[writer] = nums[reader] writer += 1 return writer Prematurely advancing writer before writing: The writer index marks the boundary where the next unique element belongs. Overwriting
nums[writer] = nums[reader]before incrementingwriter += 1is required, or else you leave gaps.Comparing
readeragainstreader - 1instead of trustedwriter - 1: In standard deduplication, comparing withreader - 1works for single duplicates, but fails to generalize to -duplicates. The canonical boundary-of-truth compares withwriter - 1.Returning
len(nums)instead ofwriter: The array is compacted in-place to indexwriter - 1; the total count of unique preserved elements iswriter, not the original array length.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Sorted array + in-place modification requirement + O(1) extra memory constraint.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: nums[0..writer-1] contains the sorted unique elements. If nums[reader] != nums[writer-1], nums[writer] = nums[reader] and writer increments.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
Comparing against
nums[writer](the next free slot) instead ofnums[writer - 1](the last kept value): on[0,0,1,1,1,2,2,3,3,4]it keeps 7 values instead of 5.
The 60-Second Interview Pitch
Say this out loud before you type a single line
"I'd use Read-Write Pointers. The array is sorted, so equal values sit next to each other, and a value is new exactly when it differs from the last value I kept. writer marks the next free slot and starts at 1, because nums[0] is always kept; reader walks from index 1 to the end. When nums[reader] differs from nums[writer - 1], I copy it to nums[writer] and move writer forward. writer never passes reader, so I only ever overwrite a slot I have already read, and nums[0] to nums[writer - 1] always holds the distinct values in order. The trap is comparing with nums[writer] instead of nums[writer - 1]: that slot is the next free one and still holds an old value, so on [1, 2, 2] the second 2 is lost and I return 1 instead of 2. It's
O(N)time andO(1)extra space, and I return writer."
Complexity & Mathematical Proof
O(N)
Look at the loop: for reader in range(1, len(nums)) visits each element once and does one comparison with nums[writer - 1]; writer only moves when a new value is copied, so each slot is written at most once. N - 1 iterations of O(1) work: O(N).
O(1) Auxiliary
Look at memory allocations: Call stack uses O(1) Iterative. In-place array manipulation overwriting indices in nums using two integer indices.
T(N) = ∑_{j=1}^N O(1) = O(N)
Derivation Progression
for reader ∈ [1, N-1]
reader visits every element after the first exactly once.
if nums[reader] != nums[writer - 1]: nums[writer] = nums[reader]; writer += 1
writer advances only when a new value is copied to the next free slot.
O(N) total steps
Array is partitioned in-place in exactly N iterations.
Variable Definitions
Total number of elements in the sorted array
Memory Architecture & Bounds
O(1) Iterative
O(1) Zero heap memory
O(1) Returns integer count
Boundary Best / Worst Cases
Pointer Invariant Transition Progression
Senior SWE Deconstruction & Hardware Caveats
Triggers: "remove duplicates in-place from sorted array", "return number of unique elements k", "do not allocate extra space". The sorted property guarantees duplicates are strictly adjacent. Reader-writer pointer compaction pattern partitions array into [0..write-1] (unique elements) and [write..N-1] (garbage/unprocessed).
. Requires strictly time and auxiliary space. Boundary choice: write pointer initializes to index 1 because the first element is unconditionally unique.
base case (loop does not execute, returns 1 correctly). Overwriting candidate elements before inspection (avoided by write <= read). Comparing candidate with nums[read-1] instead of nums[write-1] (both work when sorted, but write-1 generalizes to k-duplicate compaction).