YeetCode
Data Structures & Algorithms · Arrays

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.

6 min readBy Bhavesh Singh
arraystwo pointer in-placein-place removalwrite pointerleetcode easy

This article has an interactive companion

Don't just read it — step through it interactively, one state change at a time.

Open the Remove Element visualizer

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.

text
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 k slots.

The brute force baseline

The instinct is to find each val and splice it out.

javascript
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:

  • read visits every element, front to back — it inspects.
  • write marks 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

javascript
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.

readnums[read]val?actionwrite afternums snapshot
03equals valskip — do nothing0[3, 2, 2, 3]
12keepnums[0] = 2, write++1[2, 2, 2, 3]
22keepnums[1] = 2, write++2[2, 2, 2, 3]
33equals valskip — do nothing2[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

ApproachTimeSpaceWhy
Splice on matchO(n²)O(1)each splice shifts up to n elements; up to n splices
Two-pointer overwriteO(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 mutate nums in place — it fails the constraint even though the numbers look correct.
  • Advancing write on every iteration. write must move only when you keep an element. Bumping it unconditionally (or putting write += 1 outside the if) copies vals into the result and returns the wrong count.
  • Comparing read and write values instead of nums[read] and val. 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 nums past index write. The grader only inspects the first k slots; touching the tail is wasted work.
  • Returning write - 1 or nums.length. write already 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.

Open the Remove Element visualizer