DSAPrep
EasyTwo Pointers

Move Zeroes

Given an integer array nums, move all 0's to the end of it while maintaining the relative order of the non-zero elements.

Note that you must do this in-place without making a copy of the array.

Example 1

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

            
                Explanation: The 1, 3 and 12 keep their relative order and the two zeros are moved to the end.
              
          

Example 2

            Input: nums = [0]
            Output: [0]
            

            
                Explanation: A single zero cannot move anywhere, so it stays in place.
              
          

Constraints

  • 1 <= nums.length <= 10^4
  • -2^31 <= nums[i] <= 2^31 - 1
Follow-up: Could you minimize the total number of operations done?
View original on LeetCode โ†—

The pattern behind this problem is the swap-partition: a slow write frontier keeps the boundary between the kept non-zeros and the zeros still being processed, while a fast scanner races ahead and is the only thing that moves each step. The insight is that zeros never need to be deleted or pushed โ€” they only need to be swapped past. Every non-zero that the scanner finds swaps into the frontier slot and pushes the frontier one step right, so the zero that was there silently drifts toward the tail. Because a swap never lifts a non-zero over another non-zero, the relative order of the non-zeros is preserved for free โ€” and because the scanner passes exactly once, the whole pass is linear.

Brute Force: Shift Zeros to the End

Time O(nยฒ)Space O(1)

The most literal reading of the problem is to find a zero and physically haul it to the back of the array. Scan with one index; whenever a zero sits at that index, shift everything after it one slot left and drop a zero into the tail slot that just opened up. After the shift, the same index must be re-inspected โ€” the element that slid into the vacated slot could itself be a zero โ€” so i only advances on the else branch. Each manufactured trailing zero is never looked at again, hence the shrinking logical tail.

class Solution:
def moveZeroes(self, nums: list[int]) -> None:
n = len(nums)
moved = 0 # how many zeros are already pinned to the tail
i = 0
while i < n - moved:
if nums[i] == 0:
# Shift the tail left one slot, then pin a zero at the back.
for j in range(i, n - 1 - moved):
nums[j] = nums[j + 1]
nums[n - 1 - moved] = 0
moved += 1
else:
i += 1

Why it is quadratic: each zero costs a shift of the entire remaining tail. An input that is mostly zeros moves almost every element almost every time โ€” with n zeros, the first grabs n - 1 shifts, the next n - 2, and so on, which sums to about nยฒ / 2 shifts in the worst case. The same tail is walked over and over even though each element only deserved one look.

Two Pointers: Swap-Partition

OptimalTime O(n)Space O(1)

Both pointers start at index 0. slow is the write frontier โ€” the index of the first zero slot, which also doubles as the count of non-zeros handled so far. fast scans left to right exactly once, and at every stop it asks one question: is this a zero? A zero is skipped โ€” slow stays frozen. A non-zero is swapped with the frontier slot and slow advances by one. The swap is the key trick: it simultaneously packs the non-zero into the kept prefix and kicks the zero toward the right side, so neither physical deletion nor a second pass is ever needed.

class Solution:
def moveZeroes(self, nums: list[int]) -> None:
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
# Push this non-zero onto the packed prefix and let the
# zero it displaces drift right of the frontier.
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1

Watch the markers on the file example, [0, 1, 0, 3, 12]: each non-zero swaps into the frontier slot and the swapped-out zero drifts right, giving [1, 3, 12, 0, 0].

setup ยท slow = 0 awaits the first non-zero

nums โ€” one array, written in place

00
โ–ฒ slowโ–ฒ fast
11
02
33
124
slow = 0 โ€” first zero slot / count of non-zeros

a non-zero is never moved twice and never reordered โ€” slow advances one slot per swap, so the kept prefix always packs left in its original relative order

1 / 7
currentcomparingresultdiscarded

The rule that makes this O(n): a slow write frontier holds the index of the first zero slot, and a fast scanner does the only move of each step. slow starts at 0, pointing at the leftmost slot. As fast reads a non-zero, that value is swapped into slot slow and slow advances by one; a zero is only skipped. After one pass every non-zero sits at the front in its original order and every zero has drifted right.

Watch the longer run [0, 0, 1, 0, 5, 0, 0, 8]: leading and middle zeros are skipped without moving the frontier, every non-zero (including the 8 that starts at the far right) is written exactly once, and the relative order [1, 5, 8] survives intact.

setup ยท slow = 0 awaits the first non-zero

nums โ€” one array, written in place

00
โ–ฒ slowโ–ฒ fast
01
12
03
54
05
06
87
slow = 0 โ€” first zero slot / count of non-zeros

a non-zero is never moved twice and never reordered โ€” slow advances one slot per swap, so the kept prefix always packs left in its original relative order

1 / 10
currentcomparingresultdiscarded

Richer example: [0, 0, 1, 0, 5, 0, 0, 8]. Two leading zeros mean the frontier slow = 0 will not move for the first two reads. Watch how a run of zeros is skipped as one unit while a single non-zero advances the frontier exactly one slot.

Why it is linear: fast advances once per element and does a constant O(1) comparison; slow advances once per non-zero and does a constant O(1) swap. Every cell is read at most twice (once read, at most once swapped), giving O(n) total time with no re-scanning โ€” a dramatic contrast to the shift-based brute force that re-walks the same tail. Space is O(1): the swap happens on the array itself, and the only other state is two indices. Correctness rests on the swap-partition invariant: all cells left of slow are non-zeros in their original relative order, so the swap at slow always lands into the first zero slot and never reorders anything. An edge case takes care of itself โ€” a single element, zero or not, leaves the frontier unmoved and the array unchanged. A common fill-zeros variant writes the first slow non-zeros and then overwrites the tail with zeros; it is equally O(n) but does more writes per zero than a swap, which is why swap is preferred when trying to minimize operations.