Reasoning Into CodeArunank Divya Sharan · Python edition← Arunank’s portfolio
Module 04 · Two Pointers and In-Place Read/Write State

033. Move Zeros Stably

Foundation · Download this solution · All solutions and shared helpers

Problem

Move all zeros to the end of a mutable integer list, preserving the relative order of nonzero values. Return the same list, with its original length. Negative numbers are nonzero and must be retained in order.

Examples

Example 1

move_zeros([0, 1, 0, 3, 12])
=> [1, 3, 12, 0, 0]

Nonzero order is unchanged.

Example 2

move_zeros([])
=> []

Both phases do no work.

Example 3

move_zeros([0, -1, 0, -2])
=> [-1, -2, 0, 0]

Negative values are ordinary retained data.

Reveal the worked solution and reasoning

Why this mechanism works

First compact accepted values into a prefix. Because write never exceeds the read position, each write is safe. After all original values have been read, every remaining position can be filled with zero. The second phase avoids swapping zeros repeatedly through the list.

State and boundaries

write is the count of nonzero values consumed; nums[:write] is their stable sequence.

Protocol

  1. Scan original values.
  2. Write each nonzero at write and advance write.
  3. After the scan fill the remaining suffix with zeros.
  4. Return nums.

Visualization

[0,1,0,3,12]
compact: [1,3,12 | ?,?]
fill:    [1,3,12 | 0,0]

Python implementation

def move_zeros(nums):
    write = 0
    for value in nums:
        if value != 0:
            nums[write] = value
            write += 1
    for i in range(write, len(nums)):
        nums[i] = 0
    return nums

Worked execution

Recorded state changes after statements; L numbers refer to the solution file. Long traces are explicitly condensed.

move_zeros([0, 1, 0, 3, 12])

L2 move_zeros: write = 0
nums=[0, 1, 0, 3, 12]; write=0

L3 move_zeros: for value in nums:
nums=[0, 1, 0, 3, 12]; write=0; value=0

L3 move_zeros: for value in nums:
nums=[0, 1, 0, 3, 12]; write=0; value=1

L5 move_zeros: nums[write] = value
nums=[1, 1, 0, 3, 12]; write=0; value=1

L6 move_zeros: write += 1
nums=[1, 1, 0, 3, 12]; write=1; value=1

L3 move_zeros: for value in nums:
nums=[1, 1, 0, 3, 12]; write=1; value=0

L3 move_zeros: for value in nums:
nums=[1, 1, 0, 3, 12]; write=1; value=3

L5 move_zeros: nums[write] = value
nums=[1, 3, 0, 3, 12]; write=1; value=3

... 4 intermediate state changes omitted ...

L7 move_zeros: for i in range(write, len(nums)):
nums=[1, 3, 12, 3, 12]; write=3; value=12; i=3

L8 move_zeros: nums[i] = 0
nums=[1, 3, 12, 0, 12]; write=3; value=12; i=3

L7 move_zeros: for i in range(write, len(nums)):
nums=[1, 3, 12, 0, 12]; write=3; value=12; i=4

L8 move_zeros: nums[i] = 0
nums=[1, 3, 12, 0, 0]; write=3; value=12; i=4

Result: [1, 3, 12, 0, 0]. Nonzero order is unchanged.

move_zeros([])

L2 move_zeros: write = 0
nums=[]; write=0

Result: []. Both phases do no work.

Why the solution is correct

The first phase appends nonzero values in encounter order without overwriting unread data. The number of unwritten positions equals the number of zeros. Filling exactly that suffix preserves length and multiplicity.

Time and space

O(n) time and O(1) auxiliary space.

Common mistakes

Swapping arbitrary nonzero endpoints can break stability. Filling zeros during the read scan can overwrite values not yet examined.

Reasoning card

State: write is the count of nonzero values consumed; nums[:write] is their stable sequence.

Invariant: The prefix before write is the final compacted output.

Decision to rehearse: Separate read position from next write position.

Finish: Return nums

Transfer exercise

Generalize zero to a discard predicate and state whether the discarded suffix must preserve order too.