Leetcode Hot 100 第四篇,轮到这道名字朴素、要求却一点不含糊的题:移动零。
它看着很像一道“把零丢后面去就完了”的热身题, 但题目专门补了一句:
必须在不复制数组的情况下原地对数组进行操作。
这句话的潜台词就是:
- 不能新开一个同等大小的数组偷懒
- 不能先筛非零、再补零直接重建
- 得在原数组上动手,把零往后挪
- 同时还得保证非零元素的相对顺序不变
所以这题最顺手的正解,就是:双指针 / 快慢指针。
题目链接
题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意,必须在不复制数组的情况下原地对数组进行操作。

解题思路:快慢指针原地交换
这题的关键,是把“非零元素往前放”这件事想明白。
我们并不需要盯着每个 0 想怎么搬家,
更高效的思路是:
扫描整个数组,看到非零元素,就把它放到前面该去的位置。
这样一来:
- 前面会逐渐变成排好队的非零元素区
- 后面剩下的位置,自然会慢慢变成零区
两个指针分别干什么?
right:负责从左到右扫描数组left:指向“下一个应该放非零元素的位置”
也就是说:
right像巡逻的left像安排座位的
一旦 right 找到一个非零数,
就把它交换到 left 的位置,
然后 left 往后走一步,准备迎接下一个非零数。
代码实现
class Solution(object): def moveZeroes(self, nums): """ :type nums: List[int] :rtype: None Do not return anything, modify nums in-place instead. """ left = 0 for right in range(len(nums)): if nums[right] != 0: nums[left], nums[right] = nums[right], nums[left] left += 1代码解析
1. left 指向下一个非零元素该放的位置
left = 0一开始,数组最前面的位置就是第一个非零数应该去的地方。
所以:
left = 0表示“下一个非零元素先往 0 号位安排”
2. right 负责遍历整个数组
for right in range(len(nums)):right 会从头扫到尾,挨个看每个元素。
3. 只处理非零元素
if nums[right] != 0:如果当前元素是 0,那就先不管它。
因为这题真正重要的,不是“怎么处理零”, 而是:
怎么把所有非零元素稳定地往前收紧。
4. 把非零元素交换到前面该去的位置
nums[left], nums[right] = nums[right], nums[left]left += 1这里有两层意思:
第一层:把当前非零元素放到前面
比如 left = 1,right = 3,
说明:
- 前面第
1个位置还空着,等着放非零元素 - 当前在第
3个位置找到了一个非零元素
那就直接交换。
第二层:left 往后走
因为当前位置已经成功安置了一个非零元素, 所以下一个非零元素就该去更后面的位置了。
示例推演
来看最经典的样例:
nums = [0, 1, 0, 3, 12]初始状态
left = 0right从0开始往后扫
right = 0
nums[right] = 0是零,跳过。
数组不变:
[0, 1, 0, 3, 12]right = 1
nums[right] = 1不是零,交换 nums[left] 和 nums[right]:
nums[0], nums[1] = nums[1], nums[0]数组变成:
[1, 0, 0, 3, 12]然后:
left = 1right = 2
nums[right] = 0还是零,跳过。
数组不变:
[1, 0, 0, 3, 12]right = 3
nums[right] = 3不是零,交换:
nums[1], nums[3] = nums[3], nums[1]数组变成:
[1, 3, 0, 0, 12]然后:
left = 2right = 4
nums[right] = 12不是零,继续交换:
nums[2], nums[4] = nums[4], nums[2]数组变成:
[1, 3, 12, 0, 0]最后结果就是:
[1, 3, 12, 0, 0]为什么这种方法能保持相对顺序不变
这一点非常重要。
题目不是只让你把 0 扔后面,
还要求:
非零元素之间原本的先后顺序不能乱。
而这个写法恰好满足。
因为 right 是从左到右扫描的,
所以我们遇到非零元素的顺序,就是它们原本出现的顺序。
每次都把当前遇到的非零元素,
依次放到 left 指向的位置上。
于是前面的非零区就保持了原有次序。
简单说就是:
- 谁先被
right遇到 - 谁就先被安排进前面的非零区
秩序很稳,不搞插队。
复杂度分析
时间复杂度
整个数组只遍历一遍,所以时间复杂度是:
O(n)空间复杂度
只用了两个额外指针变量,没有新建数组,所以空间复杂度是:
O(1)这也正好满足题目要求的“原地操作”。
你的写法和标准写法的关系
你给的代码本质上也是双指针思路,方向没跑偏:
left = 0n = len(nums)for right in range(n): tmp = nums[left] nums[left] = nums[right] nums[right] = tmp if nums[left] != 0: left += 1return nums它很多情况下也能得到正确结果, 因为你其实也是在不断把非零元素往前换。
但从题解表达上看,标准写法更推荐下面这种:
1. 先判断是不是非零,再交换
if nums[right] != 0: nums[left], nums[right] = nums[right], nums[left] left += 1这样逻辑更清楚:
- 只有非零元素才参与前移
- 零元素直接跳过
2. 不需要返回数组
这题要求是:
modify nums in-place instead
也就是说函数直接修改原数组即可,
不需要再 return nums。
当然,写了返回值在很多语言环境下也不一定报错, 但从题意和规范表达上,最好别多此一举。
这种写法的关键点
1. 题目要你“移动零”,本质却是“收紧非零”
别被题目名字带偏。
真正高效的思路,不是盯着零怎么搬, 而是让非零元素一个个去前面坐好。
2. left 永远指向非零区的下一个空位
这就是整个算法的核心定位。
3. 原地操作不代表不能交换
原地操作只是说:
- 不额外开新数组
- 直接在原数组上修改
交换恰好就是最自然的一种原地处理方式。
小结
这题不难,但特别适合建立双指针的基本手感。
如果只记一句话,那就是:
快指针负责找非零,慢指针负责安排座位。
于是整个过程就很顺了:
- 扫到零,先略过
- 扫到非零,就往前放
- 扫完一遍,零自然全被挤到后面
Hot 100 第四篇,继续推进。 别小看这种基础题,很多高楼大厦,最开始都是从把这几个零挪明白开始盖起来的。🦐