Lesson 6 of 30
In-Place Array Manipulation
Move, remove, and compact array elements without allocating a second array — the read/write pointer technique behind moving zeroes, removing values, and rotating a list.
A huge share of "easy" array interview questions — and a real share of actual frontend bugs — come down to one skill: rearranging an array's contents without allocating a second one. This lesson is about the read/write pointer technique that makes that possible, and about a genuine bug it prevents.
The core technique: read pointer, write pointer
You saw the shape of this in the two-pointers lesson for deduplication. It generalizes to any "keep some, drop some, compact the rest" problem:
function moveZeroesToEnd(arr) {
let writeIndex = 0;
// Pass 1: compact every non-zero value to the front, in order.
for (let readIndex = 0; readIndex < arr.length; readIndex++) {
if (arr[readIndex] !== 0) {
arr[writeIndex] = arr[readIndex];
writeIndex++;
}
}
// Pass 2: fill everything after the compacted prefix with zero.
for (; writeIndex < arr.length; writeIndex++) {
arr[writeIndex] = 0;
}
return arr;
}readIndex looks at every element unconditionally. writeIndex only
advances when something worth keeping is found — so at any moment,
arr[0..writeIndex) is the compacted, correctly-ordered prefix of values kept
so far. Two sequential linear passes: O(n) time total, and because both
passes write into the same array, O(1) auxiliary space — no second array,
no .filter() that would allocate one.
The bug this prevents: mutating an array while indexing forward through it
Here's the trap, written the way it actually shows up in a pull request:
// BUGGY — do not do this
function removeAllZeroesBuggy(arr) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === 0) {
arr.splice(i, 1); // removes the element, shifts everything after it down by one
}
}
return arr;
}
removeAllZeroesBuggy([0, 0, 1, 2]); // → [0, 1, 2] — WRONG, a zero survivesTrace it: i = 0, arr[0] is 0, splice removes it — the array is now
[0, 1, 2] and everything shifted left by one. But the loop increments to
i = 1 next, which now points at 1, having silently skipped the second
zero that slid into position 0. This is a real, common bug, and the fix is
exactly the read/write technique above — a single forward pass that never
mutates the array's length mid-iteration, only its contents.
(If you truly need splice-based removal, iterating backward also
avoids the bug, because removing an element only shifts earlier indices you
haven't visited yet — but the read/write pattern is the one that generalizes
to more than just deletion.)
Rotating an array in place
A three-reversal trick rotates an array by k positions using only the
in-place reversal from the two-pointers lesson, with no second array at all:
function reverse(arr, start, end) {
while (start < end) {
[arr[start], arr[end]] = [arr[end], arr[start]];
start++; end--;
}
}
function rotateRight(arr, k) {
const n = arr.length;
k = k % n; // rotating by n is a no-op, so normalize k first
reverse(arr, 0, n - 1); // reverse everything
reverse(arr, 0, k - 1); // reverse the first k (now-correct) elements back
reverse(arr, k, n - 1); // reverse the remaining elements back
return arr;
}Why this works: reversing the whole array puts everything in the right
"neighborhood" but backward. Reversing the first k and the remaining n-k
sub-ranges individually un-reverses each piece internally while leaving the
overall rotation intact. Three linear reversals, each O(n), still sum to
O(n) time — and because every reversal happens on the original array's
slots, it's O(1) space, compared to a [...arr.slice(-k), ...arr.slice(0, -k)]
approach that is O(n) time and O(n) space, doing the same job with real
extra allocation.
What to remember
- The read/write pointer pattern compacts, filters, or rearranges an array in O(n) time and O(1) auxiliary space, with no second array.
- Mutating an array's length (via splice/shift/unshift) while indexing forward through it with the same loop is a real bug — indices shift under you and you silently skip elements. Iterate backward, or use read/write compaction instead.
- Three in-place reversals rotate an array in O(n) time, O(1) space — cheaper than building the rotated result with slice-and-concat, which costs O(n) space for the same O(n) time.
- "In place" is a genuine trade: you mutate the input, which is sometimes exactly right and sometimes exactly forbidden (React state, anything shared with a caller who still needs the original) — know which case you're in before reaching for it.
Check yourself
4 questions · pass 3/4 to unlock Prefix Sums for Fast Range Queries
1.In this in-place 'move zeroes to the end' function, what does the writeIndex variable represent at any point during the loop? function moveZeroesToEnd(arr) { let writeIndex = 0; for (let readIndex = 0; readIndex < arr.length; readIndex++) { if (arr[readIndex] !== 0) { arr[writeIndex] = arr[readIndex]; writeIndex++; } } for (; writeIndex < arr.length; writeIndex++) arr[writeIndex] = 0; return arr; }
2.What is the time and space complexity of the moveZeroesToEnd function above?
3.Why can't you simply
splice()out elements from an array while iterating forward over it with a normal for loop and expect correct results?4.A function rotates an array's elements to the right by k positions using three in-place reversals (reverse the whole array, then reverse the first k, then reverse the rest). What is its space complexity compared to a version using
arr.slice()to build the rotated result?
4 left to answer