移动零:把非零元素按序搬到前面
「把 0 移到末尾」等价于「把非零元素依次搬到前面,末尾补 0」。
维护慢指针 slow 指向「下一个非零元素该放的位置」,快指针 fast 扫描全数组:遇到非零值就与 nums[slow] 交换并前移 slow。一趟下来所有非零元素按原序聚在头部,其余自然是 0。
给你数组 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,交换正好把它们向右「拨」过去。
为什么交换而不是直接写
覆盖写法(像 LC26 那样直接把非零写到 slow 处)也能完成前半部分,但要在末尾手动补 0。
交换写法则自动完成补 0:fast 和 slow 之间恰好全是 0,交换即把 0 拨到右边。
两种都 O(n),交换写法不需要最后的清零循环,代码更整齐。
七行 Go:双指针交换
func moveZeroes(nums []int) {slow := 0for 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 同属「原地分拣」家族,只是判定条件不同。