当前:LC283 · 移动零 · 首次出现于 Day 1 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC283 · Move Zeroes · 数组 / 双指针

移动零:把非零元素按序搬到前面

「把 0 移到末尾」等价于「把非零元素依次搬到前面,末尾补 0」。

维护慢指针 slow 指向「下一个非零元素该放的位置」,快指针 fast 扫描全数组:遇到非零值就与 nums[slow] 交换并前移 slow。一趟下来所有非零元素按原序聚在头部,其余自然是 0。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

给你数组 nums,请在不复制数组的前提下,把所有 0 移动到末尾,同时保持非零元素的相对顺序。

比如 [0,1,0,3,12] 变成 [1,3,12,0,0]。

这篇文章会先给出「数 0」的等价视角,再讲双指针交换法,最后把它和 LC26/LC27 连成一条线。

先明确问题

输入 [0,1,0,3,12],输出 [1,3,12,0,0]。

约束:必须原地(O(1) 空间),非零元素相对顺序不变。

注意「相对顺序不变」这个要求——它决定了能不能用「交换到尾部」那种乱序技巧。不能,所以要用稳定双指针。

视角转换:不是移 0,而是搬非零

「把 0 移到末尾」听起来像在处理 0,但换个说法就顺了:把所有非零元素按原序搬到头部,剩下的位置自动填 0。

这个转换把「移动」变成了「按顺序填充」,而「按顺序填充非零元素」正是我们熟悉的双指针形状。

数学上两者完全等价:非零元素的位置固定后,空出来的位置必然全是 0。

转换题目说「移 0」,你该做的是「搬非零」。目标的措辞不一定是操作的方向。

双指针:快指针发现非零,慢指针安排位置

slow 指向「下一个非零元素该去的位置」,从 0 开始;fast 扫描全数组。

fast 遇到非零值:nums[slow] 和 nums[fast] 交换(或覆盖),slow 前移。

fast 遇到 0:跳过。

手推 [0,1,0,3,12]:

fast=0,值是 0,跳过;slow=0。

fast=1,值是 1 ≠ 0,与 nums[0] 交换 → [1,0,0,3,12],slow=1。

fast=2,值是 0,跳过。

fast=3,值是 3,与 nums[1] 交换 → [1,3,0,0,12],slow=2。

fast=4,值是 12,与 nums[2] 交换 → [1,3,12,0,0],slow=3。

完成。fast 和 slow 之间夹着的全是 0,交换正好把它们向右「拨」过去。

快指针遇非零就与慢指针位置交换:0 自动被拨到右边
0
[0]
1
[1]
0
[2]
3
[3]
12
[4]
fast
slow

为什么交换而不是直接写

覆盖写法(像 LC26 那样直接把非零写到 slow 处)也能完成前半部分,但要在末尾手动补 0。

交换写法则自动完成补 0:fast 和 slow 之间恰好全是 0,交换即把 0 拨到右边。

两种都 O(n),交换写法不需要最后的清零循环,代码更整齐。

七行 Go:双指针交换

solution.goGo
func moveZeroes(nums []int) {
slow := 0
for fast := 0; fast < len(nums); fast++ {
if nums[fast] != 0 {
nums[slow], nums[fast] = nums[fast], nums[slow]
slow++
}
}
}

1slow 是下一个非零元素的位置。

2fast 扫描全部元素。

3非零值就与 slow 处交换,慢指针前进。

4交换让 0 自动右移,无需额外清零。

总结

遇非零就与慢指针交换:非零按序聚前,零自动靠后。

  • 把「移 0」翻译成「搬非零」,问题立刻变成熟悉的双指针。
  • 交换写法让补 0 自动发生,比覆盖+清零更简洁。
  • 与 LC26、LC27 同属「原地分拣」家族,只是判定条件不同。
同族题目
LC26删除有序数组中的重复项LC27移除元素LC905按奇偶排序数组