YeetCode
Data Structures & Algorithms · Arrays

Move Zeroes: The In-Place Two-Pointer Pattern

Move all zeroes to the end of an array in-place while keeping non-zero order — the write-pointer two-pointer solution with a walkthrough, code, and complexity.

6 min readBy Bhavesh Singh
arraystwo pointersin-placetwo pointer in-placeleetcode easy

This article has an interactive companion

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

Open the Move Zeroes visualizer

Move Zeroes looks like a warm-up, and it is — but it's the cleanest introduction to a pattern you'll reuse constantly: the in-place write pointer. The trick is walking one array with two indices that move at different speeds, one reading every element and one marking where the next "kept" value belongs.

The catch that trips people up is the phrase in-place. You're not allowed to spin up a second array and copy the answer back. That single constraint is what turns a trivial filter into a genuine two-pointer exercise, and it's exactly the muscle interviewers are probing for.

Get the reasoning here and Remove Element, Remove Duplicates, and half the "partition an array" problems stop feeling like separate puzzles.

The problem

Given an integer array nums, move every 0 to the end while keeping the relative order of the non-zero elements. You must do it in-place — no copy into a fresh array.

text
Input: nums = [0, 1, 0, 3, 12] Output: [1, 3, 12, 0, 0]

Two words in that statement carry all the weight:

  • Relative order1, 3, 12 must stay in that sequence. You can't just count the zeroes and dump the non-zeroes in any order.
  • In-place — the answer has to live in the original array. That rules out the obvious "build a filtered list" approach and forces O(1) extra space.

The brute force baseline

The instinctive move is to filter the non-zeroes into a new array, then pad it with the right number of zeroes:

javascript
function moveZeroes(nums) { const result = []; for (const x of nums) { if (x !== 0) result.push(x); } while (result.length < nums.length) { result.push(0); } for (let i = 0; i < nums.length; i++) { nums[i] = result[i]; } }

This is correct and O(n) time, but it quietly breaks the rules: result is a second array holding up to n elements, so it costs O(n) extra space. When an interviewer says "in-place," they mean the extra memory should be constant no matter how big the input gets. This version fails that bar, and the whole point of the problem is to do better.

The key insight

Stop thinking about zeroes and think about the non-zeroes. If you scan left to right and pack every non-zero value tightly at the front — in the order you meet them — the zeroes have nowhere to go but the tail.

That means two indices, both moving forward through the same array:

  • read visits every element, one step at a time.
  • write marks the slot where the next non-zero value should land. It only advances when you actually place something.

When read lands on a non-zero, you swap it into the write slot and bump write. When read lands on a zero, you do nothing and let read move on. Because write never runs ahead of read, everything before write is already the compacted non-zero prefix, and the swap naturally pushes zeroes rightward.

Swapping (rather than overwriting) is what makes it truly in-place: the zero that was sitting at write gets carried out to read's old spot instead of being destroyed.

The optimal solution

javascript
var moveZeroes = function(nums) { let write = 0; for (let read = 0; read < nums.length; read += 1) { if (nums[read] !== 0) { // swap nums[write] and nums[read] const temp = nums[write]; nums[write] = nums[read]; nums[read] = temp; write += 1; } } };

One pass, one pointer that reads and one that writes. The write pointer is the entire idea: it's a running boundary between "already compacted" and "not yet processed." Every non-zero crosses that boundary exactly once. Notice there's no separate step to fill zeroes at the end — the swaps do it for free, because whenever read is ahead of write, the value being swapped into the tail is guaranteed to be a zero we already skipped over.

Walkthrough

Tracing nums = [0, 1, 0, 3, 12]. write starts at 0.

readnums[read]Actionwritenums after
00zero → skip0[0, 1, 0, 3, 12]
11non-zero → swap nums[0]↔nums[1], write++1[1, 0, 0, 3, 12]
20zero → skip1[1, 0, 0, 3, 12]
33non-zero → swap nums[1]↔nums[3], write++2[1, 3, 0, 0, 12]
412non-zero → swap nums[2]↔nums[4], write++3[1, 3, 12, 0, 0]

The subtle row is read = 1. write is still 0, pointing at a zero, so swapping 1 into slot 0 shoves that zero out to slot 1 — where read already stands. Nothing is lost, and 1 lands exactly where it belongs. By the final step the non-zeroes 1, 3, 12 sit in their original order and both zeroes have sunk to the back: [1, 3, 12, 0, 0].

Complexity

ApproachTimeSpaceWhy
Filter into new arrayO(n)O(n)second array holds up to n values — not in-place
Two-pointer swapO(n)O(1)single pass, only two integer indices of extra state

The optimal version touches each element once with read and does a constant-time swap at most once per element, so time is linear. The only extra memory is the two loop counters — no matter how large nums grows, the extra space stays fixed at O(1).

Common mistakes

  • Overwriting instead of swapping. Writing nums[write] = nums[read] and then setting trailing slots to zero can work, but if you overwrite without carrying the old value back, you clobber data. The swap keeps every element accounted for.
  • Advancing write on every iteration. write must only move when you place a non-zero. Bump it on zeroes too and the compaction boundary drifts, leaving gaps.
  • Breaking relative order. Sorting, or swapping from both ends inward, reorders the non-zeroes. A single left-to-right write pointer is what preserves the original sequence.
  • Assuming there's a separate zero-filling loop. Beginners often add a second pass to write zeroes at the end. The swaps already did it — an extra pass is wasted work and a common source of off-by-one bugs.

Where this pattern shows up next

The read/write two-pointer is a workhorse. Once it clicks here, these are the natural follow-ups:

You can also step through Move Zeroes interactively to watch the write pointer plow non-zeroes forward while the zeroes sink to the tail, one swap at a time.

FAQ

Why can't I just filter the zeroes out and rebuild the array?

You can, and it's O(n) time, but it uses a second array to hold the non-zero values — that's O(n) extra space and it violates the in-place requirement. The whole point of Move Zeroes is to rearrange the original array using only a constant amount of extra memory. The two-pointer swap does exactly that: it reorders elements inside nums itself with nothing but two integer indices.

What is the time and space complexity of the optimal solution?

O(n) time and O(1) space. The read pointer visits each of the n elements once, and every element triggers at most one constant-time swap. The only extra memory is the two loop variables read and write, so the space cost never grows with the input size.

How does swapping preserve the order of the non-zero numbers?

Because read and write both move strictly left to right, and write never gets ahead of read. Each non-zero value is swapped into the earliest free slot in the order you encounter it, so 1, 3, 12 land in the front in exactly their original sequence. The values swapped backward are always zeroes you've already passed, so no non-zero ever jumps ahead of another.

Do I need a second loop to fill the zeroes at the end?

No. That's the elegant part — the swaps handle it automatically. Whenever read is ahead of write, the element being swapped into the tail is a zero you skipped earlier, so the back of the array fills with zeroes as a side effect. Adding a separate zero-filling pass is redundant and a frequent source of off-by-one errors.

Make it stick: run this one yourself

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

Open the Move Zeroes visualizer