移动零:从笨办法到双指针,一次遍历的极致优雅
一道堪称“双指针”教科书级别的题目,看起来人人会做,但你能写出最优解吗?
🌱 开篇:一道简单题,为什么值得深挖?
先来看题目描述(LeetCode 283. 移动零):
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。必须原地操作,不能拷贝额外的数组。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
老实说,第一次见到这道题,我脑子里蹦出的第一个想法是:新建一个数组不就完事儿了吗?
但题目要求原地操作,这就有点意思了。在面试和刷题中,“简单题”往往是面试官观察你编码习惯、优化思维和细节把控能力的最好窗口。
今天这篇文章,我们就从最笨的解法出发,一步步迭代到双指针的最优实现,不仅写出代码,更要理解背后的优化思想。看完你会发现,原来一个“移动零”就能把双指针的精髓讲得明明白白。
🧱 解法一:额外数组法(青铜)
思路最简单:遍历原数组,把非零元素依次放进一个新数组,然后补零,最后再复制回原数组。
def moveZeroes(nums):
temp = []
for num in nums:
if num != 0:
temp.append(num)
# 补零
while len(temp) < len(nums):
temp.append(0)
# 拷贝回去
for i in range(len(nums)):
nums[i] = temp[i]
- 时间复杂度:O(n) —— 遍历几次都是线性。
- 空间复杂度:O(n) —— 多开了一个等长数组。
显然,违反了“原地操作”的要求。题目明确说了不能拷贝额外数组,所以这只能算是我们的思维起点,面试写出这个大概率过不了。
但它让我们想清楚了一个核心任务:把非零元素按顺序搬到前面,后面再补零。 能不能就在原数组上做这件事?
🥈 解法二:双指针·两次遍历(白银)
既然不能开新数组,我们就用一个指针 idx 来标记下一个非零元素应该放置的位置。
第一次遍历:把所有非零元素按顺序放到 idx 的位置,然后 idx 后移。
第二次处理:从 idx 到数组末尾全部赋值为 0。
def moveZeroes(nums):
idx = 0
# 第一次遍历:放置所有非零元素
for i in range(len(nums)):
if nums[i] != 0:
nums[idx] = nums[i]
idx += 1
# 第二次遍历:剩余位置补零
for i in range(idx, len(nums)):
nums[i] = 0
我们拿示例 [0,1,0,3,12] 走一遍:
初始: idx=0
i=0, nums[0]=0 → 跳过
i=1, nums[1]=1 → nums[0]=1, idx=1
i=2, nums[2]=0 → 跳过
i=3, nums[3]=3 → nums[1]=3, idx=2
i=4, nums[4]=12 → nums[2]=12, idx=3
此时数组变为 [1,3,12,3,12] ,最后从 idx=3 开始补零:
nums[3]=0, nums[4]=0 → [1,3,12,0,0]
- 时间复杂度:O(n),每个元素访问常数次。
- 空间复杂度:O(1),完美满足原地要求。
这个解法已经可以完美通过 LeetCode 了,代码清晰、无冗余。
但仔细想想:能不能只遍历一次就搞定? 我们第二次遍历纯粹是在补零,如果能在移动非零元素的同时把零“推”到后面,岂不更优雅?
🥇 解法三:双指针·一次遍历交换(黄金)
这里要引入经典的快慢指针思想:
- 慢指针
slow:指向当前已经处理好的非零序列的下一个位置,也就是第一个0所在的位置(或者待写入的位置)。 - 快指针
fast:遍历整个数组,负责发现非零元素。
每当 fast 遇到一个非零元素,我们就交换 nums[slow] 和 nums[fast],然后 slow 后移一位。这样非零元素就被逐步交换到前面,而零则被交换到后面。
def moveZeroes(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
再走一遍示例 [0,1,0,3,12]:
初始 slow=0
fast=0, nums[0]=0 → 跳过
fast=1, nums[1]=1 → 交换 nums[0]和nums[1] → [1,0,0,3,12], slow=1
fast=2, nums[2]=0 → 跳过
fast=3, nums[3]=3 → 交换 nums[1]和nums[3] → [1,3,0,0,12], slow=2
fast=4, nums[4]=12 → 交换 nums[2]和nums[4] → [1,3,12,0,0], slow=3
结束。
整个过程一气呵成,一次遍历,原地完成。
而且这个模板非常容易记忆:快指针找非零,慢指针等交换。
💡 一个小优化
当 slow == fast 时,其实我们不需要交换,因为元素在正确的位置上。加上判断可以减少不必要的操作:
def moveZeroes(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
if slow != fast:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
虽然时间复杂度仍是 O(n),但常数级别的写操作次数在某些极端情况下会减少。
🔬 深入分析:两种双指针方法的操作次数对比
很多同学会问:解法二和解法三都是 O(n) 时间、O(1) 空间,到底哪个更好?
这就涉及到对“写操作”次数的较真了,也是面试官喜欢追问的细节。
1. 两次遍历法(先搬后补零)
- 非零元素:每个非零元素赋值一次(
nums[idx] = nums[i])。 - 补零阶段:从
idx到末尾,每个位置赋值一次。 - 总写操作次数 ≈
非零个数 + 零的个数= n 次。
实际上,如果数组前部有大量零,非零元素会被搬运一次,零也会被重新赋值为零一次。操作次数稳定在 n 左右。
2. 一次遍历交换法
- 每次交换涉及三次赋值操作(如果使用 Python 的
a, b = b, a底层也是创建临时变量交换)。 - 最坏情况:前面全部是零,后面全部是非零,那么每个非零元素都需要和 slow 交换一次,共
非零个数次交换。
总赋值次数 =3 × 非零个数,最大可能接近 3n。 - 最好情况:数组已经有序(非零全在前),此时
slow == fast,我们优化后不做交换,赋值次数为 0(不算遍历索引操作)。
结论:
- 如果只看时间复杂度,两者都是 O(n)。
- 如果关注写操作次数,两次遍历法通常更优,尤其是当零很多、数组很大时,它只有一次赋值操作,而交换法可能有三次。
- 但交换法的代码更加紧凑、一次遍历,思路的通用性更强(很多题目都需要用交换完成原地移动)。
在真实面试中,能讲出这两种实现的差异和操作次数分析,是很大的加分项。
🚀 举一反三:从移动零到双指针万能模板
移动零的双指针思想,其实是解决**“数组分区”**问题的一把利刃。
核心思路都是:慢指针维护“已处理区域”的边界,快指针负责探查未处理区域。
同类变种题
-
移除元素(LeetCode 27)
把指定值val移到末尾,但不要求保持非 val 元素的相对顺序?
→ 可以用左右双指针碰撞,将右边的非 val 元素换到左边的 val 位置。
如果要求保持顺序,则用快慢指针(同移动零)。 -
删除有序数组中的重复项(LeetCode 26)
慢指针指向最后一个不重复元素的位置,快指针找下一个不同的元素,找到后慢指针前移并赋值。 -
删除有序数组中的重复项 II(LeetCode 80)
允许最多出现两次,依然可用快慢指针,加一个计数变量。
你会发现,移动零刚好是这类问题里最简洁的模型,因为它只有两类值:零和非零,且要保持非零的相对顺序。掌握它,再复杂的数组分区问题都能迎刃而解。
🧠 还有更骚的操作吗?
有些同学可能会问:能不能用左右指针夹逼?
比如左指针找零,右指针找非零,然后交换?
但这样会打乱非零元素的相对顺序!题目严格要求保持顺序,所以左右指针并不适用。除非题目放宽条件,不然就要守住快慢指针的方案。
另外,如果数组中不只有零,还有负数、其他特殊值,要求将零移到最后,负数移到前面,但保持各自相对顺序,怎么办?
这就升级为“荷兰国旗问题”的变种,需要用三指针(low, mid, high)来解决。刷题之路永无止境呀~
✨ 总结:一个“移动零”,万行代码基
从青铜到王者的这段旅程,我们经历了:
| 解法 | 核心思路 | 时间 | 空间 | 特点 |
|---|---|---|---|---|
| 额外数组 | 新建数组收集非零再补零 | O(n) | O(n) | 违反原地要求 |
| 双指针两次遍历 | 先搬非零,再补零 | O(n) | O(1) | 操作数少,思维直接 |
| 双指针一次交换 | 快慢指针边找边换 | O(n) | O(1) | 代码简洁,一次遍历 |
双指针的核心心法只有一句话:
慢指针守住“已完成区域”的边界,快指针在前面探路,把符合条件的元素交换或赋值到慢指针的位置。
下次再看到数组重排、去重、移除元素,不妨先想想能不能用快慢指针优雅解决。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注~
在评论区留下你的想法,或者聊聊你遇到过的“看似简单实则暗藏杀机”的面试题,我们一起探讨。🧑💻
刷题不止,思考不息,我们下一题见!