A slow write pointer trails a fast read pointer. Every non-zero it finds is swapped down to the write slot, in place.
Push every zero to the end of the array while keeping the non-zeroes in their original order. Do it in place, with no extra array. A fast `read` pointer scans every slot; a slow `write` pointer marks where the next non-zero belongs. Every non-zero found is swapped down into the write slot, so [0, write) is always the finished prefix.
`read` visits every index exactly once and answers one question: is this value non-zero? `write` only advances when a non-zero is placed, so it always points at the first slot that has not been claimed yet. Everything left of `write` is settled; everything from `write` up to `read` is the run of zeroes being pushed rightwards.
read=0 sees 0 → skip. read=1 sees 1 → swap into slot 0, write=1. read=2 sees 0 → skip. read=3 sees 3 → swap into slot 1, write=2. read=4 sees 12 → swap into slot 2, write=3. Result: [1, 3, 12, 0, 0], with three real swaps and no extra memory.
When read === write the value swaps with itself. That is a no-op, and exactly why no "is it already in place?" branch is needed. When read > write, slot `write` is guaranteed to hold a zero (every non-zero before it was already claimed), so the swap only ever trades a non-zero forward for a zero backward. Relative order survives because non-zeroes are placed in the order they are met.
One pass, one comparison and at most one swap per index, so the time is O(n). Both pointers are plain integers and the array is rewritten in place, so the extra space is O(1).
The same read/write partition powers a whole family of in-place array problems: remove element, remove duplicates from a sorted array, and partitioning around a pivot. Whenever you are asked to compact an array in place while keeping order, reach for a slow write pointer trailing a fast read pointer.