You're solving "Remove Duplicates from Sorted Array" (LeetCode #26). You see the solution:
def removeDuplicates(nums):
if not nums:
return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return writeIt works, but the logic feels backwards. Why does write start at 1? Why are we comparing nums[read] with nums[read - 1]? What's actually happening?
This is the read/write pointer pattern—a specific variant of two pointers for in-place array manipulation. Once you understand the intuition, it becomes crystal clear.
This guide will explain the read/write pointer concept, walk through a step-by-step example, and show you how to apply this pattern to similar problems.
TL;DR
- Read pointer: Scans through the array, examining each element
- Write pointer: Marks where to place the next unique element
- Pattern: Read scans ahead, write stays behind, only advancing when we find a new unique element
- Key insight: We're building a new array in-place, overwriting the old values
- Return value: The write pointer's final position is the length of the new array
The Intuition: Building a New Array In-Place
The Naive Approach (Extra Space)
If we had extra space, removing duplicates would be simple:
def removeDuplicates(nums):
result = []
for i, num in enumerate(nums):
if i == 0 or num != nums[i - 1]:
result.append(num)
return resultExample:
Input: [1, 1, 2, 2, 3]
Output: [1, 2, 3]Simple! But this uses O(n) extra space.
The In-Place Approach (Read/Write Pointers)
The problem requires O(1) space. We must modify the array in-place.
Key insight: We can build the result array in the same array, overwriting values as we go.
The pattern:
- Read pointer: Scans through the array
- Write pointer: Marks where to write the next unique value
Visual concept:
[1, 1, 2, 2, 3]
W R Read finds duplicate (1 == 1), don't write
[1, 1, 2, 2, 3]
W R Read finds new value (2 != 1), write it
[1, 2, 2, 2, 3]
W R Read finds duplicate (2 == 2), don't write
[1, 2, 3, 2, 3]
W R Read finds new value (3 != 2), write it
Result: [1, 2, 3, ?, ?]
W
Length = 3 (write pointer position)Step-by-Step Walkthrough
Let's trace through the algorithm with nums = [1, 1, 2, 2, 3]:
Initial State
nums = [1, 1, 2, 2, 3]
write = 1 # Start at index 1
read = 1 # Start at index 1Why start at 1?
- The first element (index 0) is always unique (nothing before it to compare)
- We keep it as-is
- Start comparing from index 1 onward
Iteration 1: read = 1
nums = [1, 1, 2, 2, 3]
^ ^
| read
write
Compare: nums[1] (1) vs nums[0] (1)
Same? Yes → Skip (don't write)
write stays at 1Iteration 2: read = 2
nums = [1, 1, 2, 2, 3]
^ ^
| read
write
Compare: nums[2] (2) vs nums[1] (1)
Same? No → New unique value!
Write nums[2] to nums[write]:
nums[1] = nums[2] = 2
write = 2
nums = [1, 2, 2, 2, 3]
^
writeIteration 3: read = 3
nums = [1, 2, 2, 2, 3]
^ ^
| read
write
Compare: nums[3] (2) vs nums[2] (2)
Same? Yes → Skip
write stays at 2Iteration 4: read = 4
nums = [1, 2, 2, 2, 3]
^ ^
| read
write
Compare: nums[4] (3) vs nums[3] (2)
Same? No → New unique value!
Write nums[4] to nums[write]:
nums[2] = nums[4] = 3
write = 3
nums = [1, 2, 3, 2, 3]
^
writeFinal State
nums = [1, 2, 3, 2, 3]
^
write = 3
Return write = 3 (length of unique elements)
First 3 elements are the result: [1, 2, 3]Why This Pattern Works
The Invariant
At any point during the algorithm:
nums[0...write-1]contains all unique elements found so farnums[write]is where we'll write the next unique elementnums[read]is the current element we're examining
The Key Comparison
if nums[read] != nums[read - 1]:Why compare with nums[read - 1]?
Because the array is sorted:
- If
nums[read] == nums[read - 1], it's a duplicate - If
nums[read] != nums[read - 1], it's a new unique value
We don't need to check all previous elements—just the one immediately before.
Why Write Pointer Stays Behind
The write pointer only advances when we find a new unique element. This ensures:
- We don't overwrite unique values we've already placed
- The write pointer always points to the next available position
- The final write position is the length of the result
The Complete Implementation
Python
def removeDuplicates(nums: List[int]) -> int:
# Edge case: empty array
if not nums:
return 0
# Write pointer starts at 1 (index 0 is always unique)
write = 1
# Read pointer scans from index 1 to end
for read in range(1, len(nums)):
# If current element is different from previous
if nums[read] != nums[read - 1]:
# Write it to the write position
nums[write] = nums[read]
# Move write pointer forward
write += 1
# Return the length of unique elements
return writeJavaScript
function removeDuplicates(nums) {
if (nums.length === 0) return 0;
let write = 1;
for (let read = 1; read < nums.length; read++) {
if (nums[read] !== nums[read - 1]) {
nums[write] = nums[read];
write++;
}
}
return write;
}Java
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int write = 1;
for (int read = 1; read < nums.length; read++) {
if (nums[read] != nums[read - 1]) {
nums[write] = nums[read];
write++;
}
}
return write;
}Visual Table Trace
| Step | read | nums[read] | nums[read-1] | Different? | Action | Array State | write |
|---|---|---|---|---|---|---|---|
| Init | - | - | - | - | - | [1,1,2,2,3] | 1 |
| 1 | 1 | 1 | 1 | No | Skip | [1,1,2,2,3] | 1 |
| 2 | 2 | 2 | 1 | Yes | Write | [1,2,2,2,3] | 2 |
| 3 | 3 | 2 | 2 | No | Skip | [1,2,2,2,3] | 2 |
| 4 | 4 | 3 | 2 | Yes | Write | [1,2,3,2,3] | 3 |
| End | - | - | - | - | Return 3 | [1,2,3,?,?] | 3 |
Common Mistakes
Mistake 1: Starting Write at 0
# WRONG
write = 0
for read in range(len(nums)):
if read == 0 or nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1Why it's wrong: This works, but it's unnecessarily complex. The first element is always unique, so we can skip it.
Better: Start write at 1, skip the first element.
Mistake 2: Comparing with nums[write - 1]
# WRONG
if nums[read] != nums[write - 1]:Why it's wrong: We should compare with the previous element in the original array (nums[read - 1]), not with what we've written (nums[write - 1]).
Mistake 3: Not Returning Write Pointer
# WRONG
return len(nums)Why it's wrong: The length of the array doesn't change. We need to return the number of unique elements, which is the write pointer's position.
Mistake 4: Forgetting Edge Cases
# WRONG: Doesn't handle empty array
write = 1
for read in range(1, len(nums)):
# ...Fix: Check for empty array first.
Variations of This Pattern
Variation 1: Remove Duplicates II (Allow 2 Duplicates)
def removeDuplicates(nums):
if len(nums) <= 2:
return len(nums)
write = 2 # Start at 2 (first two are always valid)
for read in range(2, len(nums)):
# Compare with element 2 positions back
if nums[read] != nums[write - 2]:
nums[write] = nums[read]
write += 1
return writeKey change: Compare with nums[write - 2] to allow up to 2 duplicates.
Variation 2: Move Zeroes
def moveZeroes(nums):
write = 0 # Position for next non-zero
for read in range(len(nums)):
if nums[read] != 0:
nums[write] = nums[read]
write += 1
# Fill remaining positions with zeros
for i in range(write, len(nums)):
nums[i] = 0Key change: Write pointer tracks non-zero elements, then fill rest with zeros.
Variation 3: Remove Element
def removeElement(nums, val):
write = 0
for read in range(len(nums)):
if nums[read] != val:
nums[write] = nums[read]
write += 1
return writeKey change: Skip elements equal to val instead of duplicates.
Time and Space Complexity
Time Complexity: O(n)
- Single pass through the array
- Constant work per element
Space Complexity: O(1)
- Only use two pointers (write and read)
- Modify array in-place
Practice Strategy
To master the read/write pointer pattern:
- Solve Remove Duplicates (#26) - basic pattern
- Solve Remove Duplicates II (#80) - allow k duplicates
- Solve Move Zeroes (#283) - partition pattern
- Solve Remove Element (#27) - remove specific value
- Manually trace each solution on paper to internalize the pattern
FAQ
Q: Why does write start at 1 instead of 0?
A: The first element (index 0) is always unique—there's nothing before it to compare. We keep it as-is and start writing from index 1.
Q: What if the array is empty?
A: Return 0. The edge case check handles this.
Q: What happens to the elements after the write pointer?
A: They're ignored. The problem only cares about the first write elements. The rest can be anything.
Q: Can I use this pattern on unsorted arrays?
A: Not for removing duplicates (you'd need a hash set). But you can use it for other in-place operations like moving zeros or removing specific values.
Q: Why not use a hash set to track seen values?
A: That would use O(n) space. This problem requires O(1) space. Also, since the array is sorted, we can detect duplicates by comparing adjacent elements.
Conclusion
The read/write pointer pattern is a powerful technique for in-place array manipulation.
Key concepts:
- Read pointer: Scans through the array
- Write pointer: Marks where to place the next valid element
- Invariant:
nums[0...write-1]contains the result so far - Return: Write pointer's position is the length of the result
The pattern:
write = 1 # or 0, depending on problem
for read in range(1, len(nums)): # or 0
if condition: # element is valid
nums[write] = nums[read]
write += 1
return writeWhy it works:
- We're building a new array in-place
- Write pointer stays behind, only advancing for valid elements
- Read pointer scans ahead, examining each element
- Final write position is the length of the result
Master this pattern, and you'll solve all in-place array manipulation problems with confidence. For more on two pointers, see the complete guide and in-place manipulation.
Next time you see "modify array in-place," think: read/write pointers.
Want to Practice LeetCode Smarter?
LeetCopilot is a free browser extension that enhances your LeetCode practice with AI-powered hints, personalized study notes, and realistic mock interviews — all designed to accelerate your coding interview preparation.
Also compatible with Edge, Brave, and Opera
