AniUI Academy

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.

9 min read

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 survives

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

Try it yourself
Loading playground...

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

up to 50
  1. 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. 2.What is the time and space complexity of the moveZeroesToEnd function above?

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