Remove Element: The Two-Pointer Overwrite Trick
Remove Element in-place with a two-pointer write scan — intuition, a worked walkthrough, JavaScript code, complexity analysis, and the common off-by-one traps.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Remove Element looks trivial until the constraints bite: delete every copy of a value from an array, in place, using O(1) extra space. No filter, no second array, no splice in a loop. That single rule — mutate the array you were handed and nothing else — is what turns a beginner exercise into the cleanest introduction to the two-pointer "read and write" pattern.
The trick is to stop thinking about deleting and start thinking about overwriting. You never remove anything. You copy the survivors toward the front and report how many there were.
Get this pattern and Move Zeroes, Remove Duplicates, and half the in-place array problems on LeetCode collapse into the same five lines.
The problem
Given an integer array nums and a value val, remove all occurrences of val from nums in place. Return k, the number of elements that are not equal to val. The first k slots of nums must hold those surviving elements; whatever sits in the array past index k doesn't matter.
Input: nums = [3, 2, 2, 3], val = 3
Output: k = 2, with nums[0..1] = [2, 2]
(the tail — nums[2..3] — is ignored)Two constraints shape everything:
- In place, O(1) extra space — you can't allocate a fresh result array.
- Order of the kept elements can stay as-is, and the leftover tail is explicitly don't-care. The grader only checks the first
kslots.
The brute force baseline
The instinct is to find each val and splice it out.
function removeElement(nums, val) {
let i = 0;
while (i < nums.length) {
if (nums[i] === val) {
nums.splice(i, 1); // shift everything after i left by one
} else {
i++;
}
}
return nums.length;
}It returns the right answer, but every splice shifts all following elements left by one — an O(n) operation. If the array is [3, 3, 3, ..., 3], you splice n times, each shifting up to n elements: O(n²) total. You're also mutating length mid-loop, which is exactly the kind of index bookkeeping that breeds off-by-one bugs. There's a cleaner way that never shifts anything.
The key insight
Removal is expensive because it forces everything behind the gap to slide. So don't create a gap. Instead, keep two pointers walking the same array:
readvisits every element, front to back — it inspects.writemarks the next slot where a survivor belongs — it commits.
read always moves. write only moves when the current element survives. Every time read lands on a value that isn't val, you copy it back to nums[write] and bump write forward. When read hits a val, you do nothing — write stays put, and the next survivor will simply overwrite that unwanted value later.
This is the two-pointer in-place overwrite pattern: one pointer reads the whole array, a slower pointer stamps down only the elements worth keeping. Because write never gets ahead of read, you're always copying from a position you've already visited — no data is ever lost.
The optimal solution
var removeElement = function (nums, val) {
let write = 0;
for (let read = 0; read < nums.length; read += 1) {
if (nums[read] !== val) {
nums[write] = nums[read];
write += 1;
}
}
return write;
};Five lines of logic. write starts at 0 because we don't yet know whether the first element survives. The loop advances read unconditionally. The if is the whole algorithm: a survivor gets copied to nums[write] and write steps forward; a val is skipped entirely. At the end, write equals the count of kept elements — which is exactly the k the problem asks for, and also the index where the meaningful part of the array ends.
Notice there's no separate "delete" step. When write lags behind read, the copy nums[write] = nums[read] silently overwrites a value we already decided to discard.
Walkthrough
Tracing nums = [3, 2, 2, 3], val = 3. Watch write stall on the 3s and advance on the 2s.
| read | nums[read] | val? | action | write after | nums snapshot |
|---|---|---|---|---|---|
| 0 | 3 | equals val | skip — do nothing | 0 | [3, 2, 2, 3] |
| 1 | 2 | keep | nums[0] = 2, write++ | 1 | [2, 2, 2, 3] |
| 2 | 2 | keep | nums[1] = 2, write++ | 2 | [2, 2, 2, 3] |
| 3 | 3 | equals val | skip — do nothing | 2 | [2, 2, 2, 3] |
Loop ends, return write = 2. The first two slots hold [2, 2] — the correct survivors. Slots nums[2..3] still read [2, 3], and that's fine: the problem told us the tail is don't-care.
The subtle row is read = 1. write was 0, so nums[0] = nums[1] overwrote the leading 3 with a 2 — the discard vanished the instant a survivor needed its slot. No shifting, no second array.
Complexity
| Approach | Time | Space | Why |
|---|---|---|---|
| Splice on match | O(n²) | O(1) | each splice shifts up to n elements; up to n splices |
| Two-pointer overwrite | O(n) | O(1) | one pass; each element does one compare and at most one copy |
The optimal version touches each element exactly once and does constant work per step — one comparison, and for survivors one assignment. No extra allocation beyond two integer pointers, so space is O(1). This is the theoretical floor: you can't remove elements without at least looking at each one.
Common mistakes
- Copying into a new array.
nums.filter(x => x !== val)returns the right values but allocates O(n) space and doesn't mutatenumsin place — it fails the constraint even though the numbers look correct. - Advancing
writeon every iteration.writemust move only when you keep an element. Bumping it unconditionally (or puttingwrite += 1outside theif) copiesvals into the result and returns the wrong count. - Comparing
readandwritevalues instead ofnums[read]andval. The gate is "does the current element equal the target value" — not any relationship between the two indices. - Trimming or worrying about the tail. You don't need to zero out or shorten
numspast indexwrite. The grader only inspects the firstkslots; touching the tail is wasted work. - Returning
write - 1ornums.length.writealready equals the survivor count. Off-by-one here is the classic slip — the pointer ends one past the last written slot, which is the length.
Where this pattern shows up next
The read/write two-pointer scan is a workhorse. Once it clicks here, these follow the same shape:
- Move Zeroes — the identical overwrite scan, then a second pass fills the tail with zeros instead of leaving it don't-care.
- Merge Sorted Array — two-pointer writing, but from the back so you never clobber unread data.
- Reverse String — two pointers converging from both ends to swap in place.
- Best Time to Buy and Sell Stock — a single pass tracking a running best, the same "walk once, keep the useful value" discipline.
You can also step through Remove Element interactively to watch the write pointer stall on each val and jump forward on each survivor.
FAQ
Why does Remove Element use two pointers instead of splice?
Splice forces every element after the removed slot to shift left by one, which is O(n) per removal and O(n²) across an array full of matches. The two-pointer overwrite never shifts anything — it copies survivors forward with a single pass, giving O(n) time and O(1) space. The write pointer simply overwrites values already marked for discard.
What does the return value k represent?
k is the number of elements not equal to val, and it also equals the final position of the write pointer. After the loop, the first k slots of nums hold the surviving elements in their original order. The problem deliberately ignores everything past index k, so you never need to clean up the tail.
Does the order of the remaining elements stay the same?
Yes. Because read walks front to back and write copies survivors in the order they're encountered, the kept elements preserve their relative order. If preserving order weren't required, you could use a faster swap-from-the-end variant, but the standard overwrite scan keeps order for free at no extra cost.
What happens when every element equals val?
The if (nums[read] !== val) check never passes, so write never advances and stays at 0. The function returns 0, correctly reporting that no elements survived. The array's contents are left untouched, which is allowed since the grader only reads the first k = 0 slots.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.