Move Zeroes — The Write Pointer Pattern Every Interview Tests [Microsoft, Amazon]
Advertisement
Problem Statement
Given an integer array
nums, move all0s to the end of the array while maintaining the relative order of the non-zero elements. You must do this in-place and with the minimum number of operations.
Example 1:
Input: [0, 1, 0, 3, 12]
Output: [1, 3, 12, 0, 0]
Example 2:
Input: [0, 0, 1]
Output: [1, 0, 0]
Constraints:
1 <= nums.length <= 10^4-2^31 <= nums[i] <= 2^31 - 1- Must be solved in-place (no copying the array to another data structure)
- Must maintain the relative order of the non-zero elements
- Why This Problem Matters
- The Diagnostic Purpose
- The Bigger Pattern
- Two Approaches
- Approach 1 — Naive (Extra Space, O(n) Space)
- Approach 2 — Optimal (In-Place, O(1) Space)
- The Write Pointer Pattern — Mental Model
- Visual Dry Run — [0, 1, 0, 3, 12]
- Why Swap vs. Overwrite — Knowing When Each Wins
- Overwrite Variant (Two-Pass Conceptually)
- Swap Variant (Single-Pass)
- Common Mistakes
- Mistake 1 — Using a Second Array
- Mistake 2 — Nested Loops (Bubble-Shifting)
- Mistake 3 — Forgetting the Fill Pass in the Overwrite Variant
- Mistake 4 — Advancing Both Pointers When read Finds a Zero
- Mistake 5 — Off-by-One in the Fill Loop
- Solutions
- Python
- JavaScript
- Complexity Analysis
- Follow-up Questions
- This Pattern Solves
- Key Takeaway
Why This Problem Matters
LeetCode 283 is deceptively simple on the surface: it is just moving some numbers around in an array. But interviewers at Microsoft and Amazon use it as a warm-up diagnostic for a very deliberate reason — and understanding that reason will change how you approach it.
The Diagnostic Purpose
When a Microsoft or Amazon engineer asks Move Zeroes in a phone screen, they are not testing whether you can write a for-loop. They are watching for three specific signals:
Do you immediately recognize the in-place constraint? A candidate who reaches for a second array has missed the most important word in the problem. The constraint forces you to think about pointer mechanics rather than brute-force space.
Do you understand the difference between overwrite and swap? The naive "collect non-zeros, then fill zeros" solution works, but a strong candidate will discuss the tradeoff — overwrite requires two conceptual passes; swap does it in one with slightly different write counts. Knowing the distinction demonstrates you think about operations, not just correctness.
Can you generalize the pattern? The best candidates finish the problem quickly and then say: "This is the same pattern as partitioning an array by a predicate — it directly applies to Sort Colors, Partition Array, and the Dutch National Flag." That observation signals pattern recognition over rote memorization, which is exactly what these companies screen for.
The Bigger Pattern
Move Zeroes is the simplest instance of what is called the write-pointer partition pattern (also called slow/fast pointer or read/write pointer). The idea generalizes to any problem of the form:
"Scan an array and compact elements satisfying condition C to the front, in their original relative order, in O(n) time and O(1) space."
Once you own this mental model, the following problems become trivial:
- LeetCode 75 — Sort Colors (Dutch National Flag): partition 0s, 1s, and 2s using two boundary pointers
- LeetCode 27 — Remove Element: compact all elements not equal to
val - LeetCode 26 — Remove Duplicates from Sorted Array: compact unique elements
- LeetCode 905 — Sort Array By Parity: separate even and odd numbers
Move Zeroes is the cleanest entry point to all of them because the predicate ("is this element non-zero?") is maximally simple. Master the pattern here; collect the rest for free.
Two Approaches
Approach 1 — Naive (Extra Space, O(n) Space)
The most natural first instinct is:
- Collect all non-zero elements into a new temporary array, preserving order.
- Fill the remaining positions of the original array with zeros.
temp = [x for x in nums if x != 0]
copy temp back into nums[0..len(temp)-1]
fill nums[len(temp)..n-1] with 0
This works correctly and runs in O(n) time. But it uses O(n) extra space for the temporary array — and the problem says "in-place." In an interview, this gets you partial credit at best. It also violates the spirit of "minimum operations" because you are doing a full separate allocation and two fill passes.
When is this acceptable? Only if you explicitly cannot modify the input array, which this problem does not require. In practice, never use this for Move Zeroes in an interview.
Approach 2 — Optimal (In-Place, O(1) Space)
The optimal approach uses two pointers that stay inside the original array:
- A
writepointer (also calledslowpointer) that tracks the next position where a non-zero element should be placed. - A
readpointer (also calledfastpointer) that scans every element of the array from left to right.
The rule is simple: whenever read encounters a non-zero element, place it at write (either by overwrite or by swap), then advance write. The read pointer always advances regardless.
By the time read reaches the end, all non-zero elements have been compacted to the front in their original order, and everything from write to n-1 either is already zero (swap variant) or needs to be filled with zeros (overwrite variant).
Time complexity: O(n) — single pass with read, at most n writes.
Space complexity: O(1) — no extra memory beyond two integer pointers.
The Write Pointer Pattern — Mental Model
Before looking at code, internalize this mental model. It applies identically to a dozen problems beyond Move Zeroes.
Imagine two cursors on the array:
nums: [0, 1, 0, 3, 12]
^
write (where the next non-zero will land)
^
read (scanning forward)
The invariant maintained throughout the algorithm is:
Everything to the left of
writeis already in its correct final form — it contains only the non-zero elements we have encountered so far, in their original order.
The read pointer's job is to find the next non-zero element and hand it to write. When read finds a zero, it does nothing and moves on. When read finds a non-zero, it places that value at write and both pointers advance.
This invariant is what makes the algorithm correct: when read exhausts the array, write sits at the boundary between "non-zeros placed correctly" and "the rest should be zeros."
Visual Dry Run — [0, 1, 0, 3, 12]
Let's trace every step. W is the write pointer, R is the read pointer. The brackets show which element R is currently examining.
Initial state:
[0, 1, 0, 3, 12]
W
R
write = 0, read = 0
Step 1 — read = 0, nums[read] = 0: Zero found. Skip. Advance read only.
[0, 1, 0, 3, 12]
W
R
write = 0, read = 1
Step 2 — read = 1, nums[read] = 1: Non-zero found! Swap nums[write] and nums[read]: swap(nums[0], nums[1]). Array becomes [1, 0, 0, 3, 12]. Advance both pointers.
[1, 0, 0, 3, 12]
W
R
write = 1, read = 2
Step 3 — read = 2, nums[read] = 0: Zero found. Skip. Advance read only.
[1, 0, 0, 3, 12]
W
R
write = 1, read = 3
Step 4 — read = 3, nums[read] = 3: Non-zero found! Swap nums[write] and nums[read]: swap(nums[1], nums[3]). Array becomes [1, 3, 0, 0, 12]. Advance both pointers.
[1, 3, 0, 0, 12]
W
R
write = 2, read = 4
Step 5 — read = 4, nums[read] = 12: Non-zero found! Swap nums[write] and nums[read]: swap(nums[2], nums[4]). Array becomes [1, 3, 12, 0, 0]. Advance both pointers.
[1, 3, 12, 0, 0]
W
R
write = 3, read = 5
Done. read has reached the end. The array is correctly arranged: [1, 3, 12, 0, 0].
Notice that after each swap involving a non-zero element, the zero that was sitting at write gets pushed back to where the non-zero came from. The zeros naturally bubble toward the end without any explicit fill pass.
Why Swap vs. Overwrite — Knowing When Each Wins
There are two valid implementations of the write pointer pattern, and interviewers love asking about the difference.
Overwrite Variant (Two-Pass Conceptually)
# Pass 1: copy non-zeros to front
write = 0
for read in range(len(nums)):
if nums[read] != 0:
nums[write] = nums[read]
write += 1
# Pass 2: fill the tail with zeros
while write < len(nums):
nums[write] = 0
write += 1
In this variant, write simply overwrites positions with non-zero values. There is no concern about what happens to the element at write — it gets clobbered. At the end, a separate fill loop zeros out the tail.
Pros: Fewer total write operations when the array has many non-zeros clustered at the front. If the array is [1, 2, 3, 0, 0], the overwrite variant touches 3 elements in the first pass and 2 in the fill — 5 total writes. The swap variant would do only 3 swaps (since write == read for indices 0, 1, 2) but still touches each element once.
Cons: Requires a second conceptual pass to fill zeros. This is not an issue for correctness, but it means slightly more code and the algorithm cannot "stop early."
Swap Variant (Single-Pass)
write = 0
for read in range(len(nums)):
if nums[read] != 0:
nums[write], nums[read] = nums[read], nums[write]
write += 1
In this variant, when a non-zero is found, you swap it into the write position. The zero (or whatever was at write) gets pushed back to read's position. Because zeros bubble backward naturally, no fill pass is needed.
Pros: Single conceptual pass, no separate fill loop. Cleaner code. Does the right thing even if write == read (swapping an element with itself is a no-op).
Cons: When most elements are non-zero, you do many self-swaps (when write == read). This is a minor performance detail that never matters in practice, but shows you have thought about operations carefully.
Interview rule of thumb: Use the swap variant by default. It is cleaner, requires no fill pass, and demonstrates the pattern more elegantly. Mention the overwrite variant when asked about minimizing write operations — some interviewers specifically ask "how would you minimize the total number of writes?"
Common Mistakes
Mistake 1 — Using a Second Array
Declaring result = [] and appending to it violates the in-place constraint. Even if you copy back at the end, you used O(n) extra space. Always work within the original array.
Mistake 2 — Nested Loops (Bubble-Shifting)
Some candidates implement a "bubble" approach: when a zero is found, shift all subsequent elements left by one position. This is O(n²) and is equivalent to bubble sort. The write pointer reduces this to O(n). If you find yourself writing a nested loop for this problem, stop and reconsider.
Mistake 3 — Forgetting the Fill Pass in the Overwrite Variant
If you use the overwrite variant and forget to zero out nums[write..n-1] at the end, you will leave stale non-zero values at the tail of the array. The swap variant sidesteps this entirely, but if you choose overwrite, the fill pass is mandatory.
Mistake 4 — Advancing Both Pointers When read Finds a Zero
The write pointer should only advance when a non-zero element is placed. A common bug is writing write += 1 unconditionally inside the loop, which causes non-zero elements to be placed at wrong positions and breaks the compaction invariant.
Mistake 5 — Off-by-One in the Fill Loop
In the overwrite variant, the fill loop should start at write (not write + 1) because write points to the first position that is NOT yet filled with the correct value. Starting at write + 1 leaves one stale element behind.
Solutions
Python
from typing import List
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Optimal swap variant — O(n) time, O(1) space.
'write' tracks where the next non-zero element belongs.
'read' scans every element.
When read finds a non-zero, swap it into the write position
so the zero at write gets pushed back. Advance write.
No fill pass needed — zeros end up at the tail automatically.
"""
write = 0 # slow pointer: next slot for a non-zero
for read in range(len(nums)): # fast pointer: scans everything
if nums[read] != 0:
# Place non-zero at write position; push the zero back
nums[write], nums[read] = nums[read], nums[write]
write += 1 # advance write only on non-zero
class SolutionNaive:
def moveZeroes(self, nums: List[int]) -> None:
"""
Naive overwrite variant — O(n) time, O(1) space,
but requires an explicit fill pass at the end.
This is still in-place (no second array), but uses
two separate conceptual passes unlike the swap variant.
"""
write = 0
# Pass 1: compact non-zeros to the front
for read in range(len(nums)):
if nums[read] != 0:
nums[write] = nums[read]
write += 1
# Pass 2: fill the remaining tail positions with zeros
while write < len(nums):
nums[write] = 0
write += 1
# --- Quick verification ---
if __name__ == "__main__":
sol = Solution()
test1 = [0, 1, 0, 3, 12]
sol.moveZeroes(test1)
assert test1 == [1, 3, 12, 0, 0], f"Expected [1,3,12,0,0], got {test1}"
test2 = [0, 0, 1]
sol.moveZeroes(test2)
assert test2 == [1, 0, 0], f"Expected [1,0,0], got {test2}"
test3 = [0]
sol.moveZeroes(test3)
assert test3 == [0], f"Expected [0], got {test3}"
test4 = [1, 2, 3]
sol.moveZeroes(test4)
assert test4 == [1, 2, 3], f"Expected [1,2,3], got {test4}"
print("All tests passed.")
JavaScript
/**
* Optimal swap variant — O(n) time, O(1) space.
*
* Two pointers within the original array:
* write — next position for a non-zero element (slow pointer)
* read — scans every element forward (fast pointer)
*
* On each non-zero, swap nums[write] and nums[read], then advance write.
* Zeros naturally bubble to the tail. No fill pass needed.
*
* @param {number[]} nums
* @return {void} — modifies nums in place
*/
function moveZeroes(nums) {
let write = 0; // slow pointer
for (let read = 0; read < nums.length; read++) { // fast pointer
if (nums[read] !== 0) {
// Swap: push zero back, pull non-zero to write position
[nums[write], nums[read]] = [nums[read], nums[write]];
write++; // only advance write when a non-zero is placed
}
// If nums[read] === 0, just advance read (implicit — loop does it)
}
}
/**
* Naive overwrite variant — also O(n) time, O(1) space.
* Two conceptual passes: compact non-zeros, then fill zeros.
* Useful when you want to explicitly minimize write operations
* on non-zero elements (each non-zero is written exactly once).
*
* @param {number[]} nums
* @return {void}
*/
function moveZeroesNaive(nums) {
let write = 0;
// Pass 1: overwrite positions with non-zero values
for (let read = 0; read < nums.length; read++) {
if (nums[read] !== 0) {
nums[write] = nums[read];
write++;
}
}
// Pass 2: zero out the tail (positions write..n-1)
while (write < nums.length) {
nums[write] = 0;
write++;
}
}
// --- Quick verification ---
const test = (fn, input, expected) => {
const arr = [...input];
fn(arr);
const ok = JSON.stringify(arr) === JSON.stringify(expected);
console.log(`${ok ? 'PASS' : 'FAIL'}: ${JSON.stringify(arr)}`);
};
test(moveZeroes, [0, 1, 0, 3, 12], [1, 3, 12, 0, 0]);
test(moveZeroes, [0, 0, 1], [1, 0, 0]);
test(moveZeroes, [0], [0]);
test(moveZeroes, [1, 2, 3], [1, 2, 3]);
Complexity Analysis
| Approach | Time | Space | Notes |
|---|---|---|---|
| Naive (extra array) | O(n) | O(n) | Fails in-place constraint |
| Overwrite (two-pass) | O(n) | O(1) | Two conceptual passes, each non-zero written once |
| Swap (one-pass) | O(n) | O(1) | One pass; zeros bubble naturally, no fill needed |
Why both in-place variants are O(n): The read pointer visits each of the n elements exactly once. The write pointer advances at most n times (only on non-zeros). Swaps or overwrites happen at most n times total. Every operation is bounded by n, so time is O(n). No data structures proportional to n are allocated, so space is O(1).
The minimum-operations argument: The overwrite variant writes each non-zero element exactly once and writes each zero exactly once (during the fill). That is at most 2n writes in the worst case. The swap variant does at most n swaps, each of which is 2 writes — also 2n writes maximum. In arrays with few zeros, the overwrite variant does fewer writes because zeros are only written once (in the fill), whereas the swap variant would have written them as part of swaps. This subtlety is the kind of nuance interviewers probe when they add "with the minimum number of operations."
Follow-up Questions
Interviewers commonly extend this problem in the following directions. Each is a direct application of the same write-pointer pattern:
1. Move zeroes to the front (instead of the end), maintain relative order of non-zeros.
Scan from right to left. The write pointer starts at n - 1 and moves left. When read finds a non-zero, place it at write (swap or overwrite). Non-zeros end up at the back in their original left-to-right order; zeros compact to the front.
2. Remove all instances of a given value val in-place (LeetCode 27).
Replace the condition if nums[read] != 0 with if nums[read] != val. The write pointer compacts everything except val to the front. The new logical length of the array is write.
3. Move all negative numbers to the front, positives to the back, maintaining relative order (Stable Partition).
Same pattern with condition if nums[read] < 0. Note: if relative order must be preserved for both groups, you need a stable algorithm. The write pointer gives you stability for the compacted group but not necessarily the other. This subtlety often leads into a discussion of merge sort-based stable partition.
4. Sort an array containing only 0s, 1s, and 2s (LeetCode 75 — Sort Colors / Dutch National Flag).
Instead of one write pointer, use two boundary pointers (low and high). low tracks where the next 0 goes; high tracks where the next 2 goes. A middle pointer mid scans forward. This is a direct two-pointer generalization of Move Zeroes.
5. Partition an array such that all even numbers come before odd numbers (LeetCode 905).
Replace the condition with if nums[read] % 2 == 0. Identical structure.
This Pattern Solves
The write-pointer partition pattern appears in each of the following. After solving Move Zeroes, revisit these with the same mental model and notice how little new thinking is required:
| Problem | LeetCode | Key Condition |
|---|---|---|
| Remove Element | 27 | nums[read] != val |
| Remove Duplicates from Sorted Array | 26 | nums[read] != nums[write - 1] |
| Move Zeroes | 283 | nums[read] != 0 |
| Sort Array By Parity | 905 | nums[read] % 2 == 0 |
| Sort Colors (Dutch National Flag) | 75 | Three-way partition with two write pointers |
| Partition Array (by pivot) | Quicksort subroutine | nums[read] <= pivot |
The more you see this pattern, the more problems collapse to it. The core idea — a write pointer that advances only when an element earns its place — is one of the most reusable primitives in array algorithms.
Key Takeaway
Move Zeroes is not a problem about zeros — it is a problem about the write-pointer partition pattern, the simplest form of in-place array partitioning. The write pointer tracks where the next "qualifying" element goes; the read pointer finds those elements. When read finishes, everything to the left of write is correct, and everything to the right can be filled or is already in place. Once this pattern is internalized, a large family of array manipulation problems — Remove Duplicates, Sort Colors, Remove Element, Partition Array — all reduce to minor variations of the same two-line core. That is why interviewers use it as a diagnostic: they are not testing the solution, they are testing whether you see the pattern.
Next: Problem 6 — Plus One →
Advertisement