移动零 图解题解
把零全赶到末尾、非零顺序不变、原地完成——fast 扫到非零就和 slow 互换,一趟解决。
像整理书架:slow 是下一个空位,fast 从左往右挑非零元素,遇到就跟 slow 位置上的数对调、slow 往右挪一格。fast 按原顺序捡起非零,依次放到最前面,书架右端自然就剩下所有被换下来的零——不用移动整段、一趟搞定。
这道题到底在问什么
- nums
- [0, 1, 0, 3, 12]
- 输出
- [1, 3, 12, 0, 0]
最优解:一步一步想明白
- 3最直觉的写法:每遇到一个 0,就把它后面的元素整体往前挪一格、再把 0 补到末尾。一个 0 就要挪一整段,最坏要挪 n 的平方次,太慢。
- 4换个思路:让两个指针分工。fast 一路向右找非零;每找到一个,就和 slow 位置交换,再让 slow 前进一格。fast 是从左到右按原顺序遇到非零的,依次落到 slow,相对顺序天然不乱;slow 左边永远是已归位的非零,末尾自然就剩 0。
- 5slow 在 0 号位等坑,fast 从头扫开局两个指针都在 0 号位。slow 记着「下一个非零应该落在哪」,fast 负责往右探路找非零。
- 6nums[0]=0 → slow 不动fast 当前指着 0。这不是非零,什么都不做,slow 留在 0 号位继续守坑,只让 fast 往右走一格。
- 7nums[1]=1 ≠ 0,准备交换fast 走到 1,是非零!该把它和 slow 守的 0 号坑交换,让 1 归位到最前。注意被点亮的两格 0 号和 1 号,就是要互换的一对。
- 8[1,0,0,3,12],slow 前进到 11 和 0 号位的 0 互换,数组变成 [1,0,0,3,12],1 归位到最前。因为放进了一个非零,slow 才前进到 1 号位,去守下一个坑。
- 9nums[2]=0 → slow 仍停 1 号位fast 又遇到 0,跳过,slow 仍停在 1 号位等下一个非零。看出节奏了吗,slow 只在「放数」那一刻才动。
- 10nums[3]=3 ≠ 0,准备交换fast 找到非零 3,准备和 slow 守的 1 号坑交换。被点亮的 1 号和 3 号就是这次互换的一对。
- 11[1,3,0,0,12],slow 前进到 23 和 1 号位的 0 互换,数组变成 [1,3,0,0,12],3 归位。slow 前进到 2 号位。前面已经稳稳是 [1,3] 了。
- 12nums[4]=12 ≠ 0,准备交换fast 走到末尾找到非零 12,准备和 slow 守的 2 号坑交换。点亮的 2 号和 4 号就是最后这对。
- 13[1,3,12,0,0],全部归位12 和 2 号位的 0 互换,数组变成 [1,3,12,0,0]。slow 左边 [1,3,12] 全是按原序归位的非零,末尾自然剩两个 0。一趟扫完,搞定。
- 14慢镜头:fast 每遇非零才交换并推动 slow把这题最该记住的一刻定格放慢:slow 左边的 [1,3] 是已归位区,slow 此刻守着 2 号坑,fast 在 3 号位探路。fast 遇非零 → 和 slow 交换 → slow 才前进,三拍一循环。删掉所有文字,光看 slow 左边越来越长、fast 一路右探,也能看懂机制。
- 17一句话本质:fast 是读指针、slow 是写指针,读到合格的就换到 slow 并前进。移除元素、删重复项、移动零,都是这套快慢指针读写分离。
- 19错误写法在 [0,1] 上翻车用一个会翻车的小例子 [0,1] 真跑错法:如果 fast=0 遇到 0 也让 slow++,slow 直接跳到 1 号位。等 fast=1 找到非零 1 时,slow 和 fast 都在 1 号位,自交换、原地不动,结果还是 [0,1],那个 0 永远没被移走。所以 slow 遇 0 绝不能动。
- 24把这题练到能复述后,去同类题迁移:改一下「保留」条件就是 LC27 移除元素、LC26 删除有序数组重复项。想多刷可让 AI 助教小欧出几道快慢指针变体,或进通关训练连着练。
⚠️ 容易写错的地方
✗ 错:slow 每轮都 +1
✓ 对:只有 nums[fast] 非零、发生交换时才 slow += 1
slow 是「下一个非零该落的坑」,遇 0 时它必须停着等。若每轮都加,坑就被空着跳过了,非零会错位
✗ 错:用覆盖 nums[slow]=nums[fast] 但忘了补 0
✓ 对:要么交换 swap,要么压缩非零后把 slow..n-1 全置 0
纯覆盖只搬走非零、不处理尾巴,后面会残留旧值,0 的个数对不上。交换法天然把 0 换到 fast 处,无需补尾
✗ 错:slow 和 fast 相同也交换
✓ 对:可不判,但要知道自己换的是同一格(无害的自交换)
数组前段没有 0 时 slow 一直等于 fast,交换的是同一格,结果不变;理解这点才不会被「为什么没动」迷惑
完整代码(Python / C++ / Java)
Python
class Solution:
def moveZeroes(self, nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1C++
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int slow = 0;
for (int fast = 0; fast < (int)nums.size(); fast++) {
if (nums[fast] != 0) {
swap(nums[slow], nums[fast]);
slow++;
}
}
}
};Java
class Solution {
public void moveZeroes(int[] nums) {
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (nums[fast] != 0) {
int t = nums[slow];
nums[slow] = nums[fast];
nums[fast] = t;
slow++;
}
}
}
}套路模板 · 快慢指针「读写分离」(挖空背骨架)
# 「原地保留/移动符合条件的元素」都套这个骨架
slow = 0 # 写指针:下一个合格元素该落的坑
for fast in range(len(nums)): # 读指针:逐个扫
if 保留条件(nums[fast]): # ← 只改这一行
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
# 移动零: 保留条件 = nums[fast] != 0
# 移除元素: 保留条件 = nums[fast] != val// 把【保留条件】替换成本题判据即可
int slow = 0;
for (int fast = 0; fast < (int)nums.size(); fast++) {
if (/* 保留条件: nums[fast] != 0 */) {
swap(nums[slow], nums[fast]);
slow++;
}
}
// slow 即最终有效长度// 把【保留条件】替换成本题判据即可
int slow = 0;
for (int fast = 0; fast < nums.length; fast++) {
if (/* 保留条件: nums[fast] != 0 */) {
int t = nums[slow]; nums[slow] = nums[fast]; nums[fast] = t;
slow++;
}
}
// slow 即最终有效长度复杂度
时间复杂度
O(n)
fast 把数组扫一遍,slow 只增不减;每个位置至多被碰一次
空间复杂度
O(1)
原地交换,只用 slow / fast 两个下标,不开额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 移动零 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么用交换而不是覆盖?两者各有什么取舍?+
覆盖法第一趟把非零写到前面、第二趟把后面补 0,写操作次数更少(非零不动则零写);交换法一趟搞定、无需补尾,但即使元素本就在原位也会做一次自交换。两者都是 O(n)、O(1),面试任选一种讲清即可。
这个解法稳定吗(非零相对顺序会不会乱)?+
稳定。fast 从左到右遇到非零,依次交换到 slow,写入顺序就是原始顺序,所以非零的相对次序不变。
能不能进一步减少写操作?+
能。交换前先判 slow != fast 再交换,跳过无意义的自交换;或用覆盖法只在有 0 时才真正搬动。但优化的是常数,复杂度仍是 O(n)。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。