moveZeroes rearranges an array so that every 0 sits at the end while the non-zero values keep their original relative order — and it does this in place, mutating the array you were given instead of building a new one. This is the classic LeetCode 283 interview problem, and the point is the in-place technique, not the result: you return the same array reference you were handed.
moveZeroes(nums)
// mutate `nums` in place — pushing the zeros to the end — then RETURN that same array
moveZeroes([0, 1, 0, 3, 12]); // → [1, 3, 12, 0, 0]
moveZeroes([0, 0, 1]); // → [1, 0, 0] leading zeros slide to the end
const a = [4, 0, 5];
moveZeroes(a) === a; // → true the return value IS the same array, now mutated
moveZeroes(a) === a is true. Do not allocate or return a new array.0 collects in one run at the tail, and the array's length never changes.0 — a value counts as a zero only when it is === 0. Falsy-but-not-zero values like false, '', and null are non-zero and stay in place.You are going to edit an array in place — walk it once, slide every non-zero toward the front, let the zeros collect at the end, and hand back that very same array, not a fresh one.
Picture a shelf of books with a few empty gaps between them. You want the books pushed together on the left, still in the order they were already in, and all the empty gaps collected on the right — without buying a second shelf. moveZeroes does exactly that to an array: the non-zero values are the books, the 0s are the gaps. You reach into the array the caller handed you, shuffle the values around, and return that same array so any variable still holding it sees the change.
To do this without allocating a second array, walk the array with two indices. A read index visits every slot left to right. A write index — call it lastNonZero — trails behind and marks where the next non-zero should land; it only moves forward when you actually place one. For each non-zero you read, you copy it to the write slot and bump the write index; each 0 you simply skip. When the scan ends, the front of the array holds all the non-zeros in order, and every slot from the write index onward is stale — so you fill those with 0. Same array object, zeros pushed to the back.
The obvious version keeps the non-zeros with filter and pads the rest with zeros:
function moveZeroes(nums) {
const kept = nums.filter((n) => n !== 0); // the non-zeros, in order
while (kept.length < nums.length) {
kept.push(0); // pad the tail back up to the original length
}
return kept; // ...but this is a BRAND-NEW array
}
The values look right — kept is [1, 3, 12] and padding gives [1, 3, 12, 0, 0]. But filter builds a new array, so you are handing back something different from what you were given. The caller's original array is untouched, and moveZeroes(a) === a is now false — anyone still holding a sees the old data with the zeros still interleaved. It also uses O(n) extra space for the second array. The whole contract is in-place mutation, and this version quietly breaks it. (A different naive attempt — repeatedly splice out a 0 and push it to the end — does mutate in place, but each splice shifts every later element, making it O(n²) and easy to get wrong when zeros sit next to each other.)
function moveZeroes(nums) {
let lastNonZero = 0; // write index: where the next non-zero should land
// Pass 1 — copy every non-zero to the front, in order.
for (let read = 0; read < nums.length; read++) {
// Only the literal 0 is a zero. `!== 0` is strict, so false, '' and
// null (falsy but not 0) count as non-zero and are kept in place.
if (nums[read] !== 0) {
nums[lastNonZero] = nums[read];
lastNonZero++;
}
}
// Pass 2 — every slot from lastNonZero to the end is stale; fill with 0.
while (lastNonZero < nums.length) {
nums[lastNonZero] = 0;
lastNonZero++;
}
return nums; // the SAME reference we were handed
}
module.exports = { moveZeroes };
The key shift from the naive version is that nothing is ever allocated. Instead of collecting the non-zeros into a new array, you overwrite nums from the front with just the non-zeros, then overwrite whatever is left with zeros. Two details make it correct and in place. First, lastNonZero never runs ahead of read — you only write after reading — so you never clobber an element you have not visited yet. Second, you only ever assign to nums[i] and never reassign nums itself, so the array's identity is preserved and moveZeroes(nums) === nums stays true. One linear pass places the non-zeros and one short pass fills the tail: O(n) time, O(1) extra space.
Trace moveZeroes([0, 1, 0, 3, 12]). Start with lastNonZero = 0.
read 0, value 0: a zero → skip. lastNonZero stays 0.read 1, value 1: non-zero → write nums[0] = 1, lastNonZero becomes 1.read 2, value 0: skip.read 3, value 3: non-zero → write nums[1] = 3, lastNonZero becomes 2.read 4, value 12: non-zero → write nums[2] = 12, lastNonZero becomes 3.The scan ends with lastNonZero = 3. The cells now read [1, 3, 12, 3, 12] — the first three are the non-zeros we packed; the last two are stale leftovers. Pass 2 fills from index 3: nums[3] = 0, then nums[4] = 0. The array is now [1, 3, 12, 0, 0]. We return nums, the same object the caller passed, so their variable reads [1, 3, 12, 0, 0] too.
return nums.filter((n) => n !== 0) drops the zeros but builds a new array. The caller's original is untouched and moveZeroes(a) === a is false. Mutate nums in place and return it.0 is a zero — match with === 0 (or !== 0), not a truthiness test. if (!nums[read]) would treat false, '', null, NaN, and 0 all as zeros and shove them to the end. Compare strictly so only 0 moves.sort.[…, 3, 12] above). Skip filling those slots with 0 and you return [1, 3, 12, 3, 12] instead of [1, 3, 12, 0, 0].write and read is zeros, so swapping a non-zero forward drops a 0 into the slot it leaves — the non-zeros stay in order and no separate fill pass is needed. When zeros are rare this touches memory the least:function moveZeroes(nums) {
let write = 0;
for (let read = 0; read < nums.length; read++) {
if (nums[read] !== 0) {
[nums[write], nums[read]] = [nums[read], nums[write]]; // swap the non-zero forward
write++;
}
}
return nums;
}
0 test with a parameter: moveValue(nums, target) that keeps nums[read] !== target. The same compaction pushes any chosen value to the end.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
moveZeroes rearranges an array so that every 0 sits at the end while the non-zero values keep their original relative order — and it does this in place, mutating the array you were given instead of building a new one. This is the classic LeetCode 283 interview problem, and the point is the in-place technique, not the result: you return the same array reference you were handed.
moveZeroes(nums)
// mutate `nums` in place — pushing the zeros to the end — then RETURN that same array
moveZeroes([0, 1, 0, 3, 12]); // → [1, 3, 12, 0, 0]
moveZeroes([0, 0, 1]); // → [1, 0, 0] leading zeros slide to the end
const a = [4, 0, 5];
moveZeroes(a) === a; // → true the return value IS the same array, now mutated
moveZeroes(a) === a is true. Do not allocate or return a new array.0 collects in one run at the tail, and the array's length never changes.0 — a value counts as a zero only when it is === 0. Falsy-but-not-zero values like false, '', and null are non-zero and stay in place.You are going to edit an array in place — walk it once, slide every non-zero toward the front, let the zeros collect at the end, and hand back that very same array, not a fresh one.
Picture a shelf of books with a few empty gaps between them. You want the books pushed together on the left, still in the order they were already in, and all the empty gaps collected on the right — without buying a second shelf. moveZeroes does exactly that to an array: the non-zero values are the books, the 0s are the gaps. You reach into the array the caller handed you, shuffle the values around, and return that same array so any variable still holding it sees the change.
To do this without allocating a second array, walk the array with two indices. A read index visits every slot left to right. A write index — call it lastNonZero — trails behind and marks where the next non-zero should land; it only moves forward when you actually place one. For each non-zero you read, you copy it to the write slot and bump the write index; each 0 you simply skip. When the scan ends, the front of the array holds all the non-zeros in order, and every slot from the write index onward is stale — so you fill those with 0. Same array object, zeros pushed to the back.
The obvious version keeps the non-zeros with filter and pads the rest with zeros:
function moveZeroes(nums) {
const kept = nums.filter((n) => n !== 0); // the non-zeros, in order
while (kept.length < nums.length) {
kept.push(0); // pad the tail back up to the original length
}
return kept; // ...but this is a BRAND-NEW array
}
The values look right — kept is [1, 3, 12] and padding gives [1, 3, 12, 0, 0]. But filter builds a new array, so you are handing back something different from what you were given. The caller's original array is untouched, and moveZeroes(a) === a is now false — anyone still holding a sees the old data with the zeros still interleaved. It also uses O(n) extra space for the second array. The whole contract is in-place mutation, and this version quietly breaks it. (A different naive attempt — repeatedly splice out a 0 and push it to the end — does mutate in place, but each splice shifts every later element, making it O(n²) and easy to get wrong when zeros sit next to each other.)
function moveZeroes(nums) {
let lastNonZero = 0; // write index: where the next non-zero should land
// Pass 1 — copy every non-zero to the front, in order.
for (let read = 0; read < nums.length; read++) {
// Only the literal 0 is a zero. `!== 0` is strict, so false, '' and
// null (falsy but not 0) count as non-zero and are kept in place.
if (nums[read] !== 0) {
nums[lastNonZero] = nums[read];
lastNonZero++;
}
}
// Pass 2 — every slot from lastNonZero to the end is stale; fill with 0.
while (lastNonZero < nums.length) {
nums[lastNonZero] = 0;
lastNonZero++;
}
return nums; // the SAME reference we were handed
}
module.exports = { moveZeroes };
The key shift from the naive version is that nothing is ever allocated. Instead of collecting the non-zeros into a new array, you overwrite nums from the front with just the non-zeros, then overwrite whatever is left with zeros. Two details make it correct and in place. First, lastNonZero never runs ahead of read — you only write after reading — so you never clobber an element you have not visited yet. Second, you only ever assign to nums[i] and never reassign nums itself, so the array's identity is preserved and moveZeroes(nums) === nums stays true. One linear pass places the non-zeros and one short pass fills the tail: O(n) time, O(1) extra space.
Trace moveZeroes([0, 1, 0, 3, 12]). Start with lastNonZero = 0.
read 0, value 0: a zero → skip. lastNonZero stays 0.read 1, value 1: non-zero → write nums[0] = 1, lastNonZero becomes 1.read 2, value 0: skip.read 3, value 3: non-zero → write nums[1] = 3, lastNonZero becomes 2.read 4, value 12: non-zero → write nums[2] = 12, lastNonZero becomes 3.The scan ends with lastNonZero = 3. The cells now read [1, 3, 12, 3, 12] — the first three are the non-zeros we packed; the last two are stale leftovers. Pass 2 fills from index 3: nums[3] = 0, then nums[4] = 0. The array is now [1, 3, 12, 0, 0]. We return nums, the same object the caller passed, so their variable reads [1, 3, 12, 0, 0] too.
return nums.filter((n) => n !== 0) drops the zeros but builds a new array. The caller's original is untouched and moveZeroes(a) === a is false. Mutate nums in place and return it.0 is a zero — match with === 0 (or !== 0), not a truthiness test. if (!nums[read]) would treat false, '', null, NaN, and 0 all as zeros and shove them to the end. Compare strictly so only 0 moves.sort.[…, 3, 12] above). Skip filling those slots with 0 and you return [1, 3, 12, 3, 12] instead of [1, 3, 12, 0, 0].write and read is zeros, so swapping a non-zero forward drops a 0 into the slot it leaves — the non-zeros stay in order and no separate fill pass is needed. When zeros are rare this touches memory the least:function moveZeroes(nums) {
let write = 0;
for (let read = 0; read < nums.length; read++) {
if (nums[read] !== 0) {
[nums[write], nums[read]] = [nums[read], nums[write]]; // swap the non-zero forward
write++;
}
}
return nums;
}
0 test with a parameter: moveValue(nums, target) that keeps nums[read] !== target. The same compaction pushes any chosen value to the end.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.