LeetCode 283. 移动零
题目描述
✅ 283. 移动零

题意分析
给定整数数组
nums,把所有0挪到数组末尾。题目同时压了两个硬性要求:一是原地修改,不能复制出一个新数组再抄回去;二是非零元素之间的相对顺序必须保持不变——[0,1,0,3,12]只能变成[1,3,12,0,0],[3,1,12,0,0]这种顺序打乱的结果不算对。换个说法,这不是排序题:
0与非零之间要分区,但非零内部的次序一根手指都不能动,也就是要求「稳定」。约束信号:数组长度可达 $10^4$,值可正可负可为
0。负数的存在提醒我们判断条件必须写!= 0,而不是> 0。边界上要留意全零数组、全非零数组和单元素数组。
解法:双指针稳定压缩非零元素
核心思路
问题关键:既要原地移动,又要保持非零元素的相对顺序。遇到一个零就整体搬移后缀会产生 $O(n^2)$ 的重复操作。
用写指针
insert表示下一个非零元素应放的位置,读指针从左到右扫描。不变量是:nums[0..insert-1]始终等于已扫描部分的全部非零元素,且顺序不变。遇到非零数就写入nums[insert]并推进写指针;读指针始终不小于写指针,因此不会覆盖尚未读取的数据。扫描结束后,
insert也是非零元素个数,把其后的区间全部补成0即可。相比交换写法,这种“稳定压缩 + 补零”更容易在面试中解释和验证。
解题步骤
- 初始化
insert = 0,表示还没有写入非零元素。- 从左到右扫描;遇到非零数,就写入
nums[insert],然后令insert++。- 扫描结束后,
nums[0..insert-1]已是按原顺序排列的全部非零元素。- 将
nums[insert..n-1]置为0,完成原地修改。以
[0,1,0,3,12]为例,第一遍依次写出1、3、12,数组前缀变为[1,3,12];再从下标3开始补零,得到[1,3,12,0,0]。
代码实现
class Solution {
public void moveZeroes(int[] nums) {
int insert = 0;
for (int num : nums) {
if (num != 0) {
// insert 左侧始终保存已压缩的非零元素。
nums[insert] = num;
insert++;
}
}
while (insert < nums.length) {
nums[insert] = 0;
insert++;
}
}
}
func moveZeroes(nums []int) {
insert := 0
for _, num := range nums {
if num != 0 {
// 非零元素按原顺序写到数组前部。
nums[insert] = num
insert++
}
}
for insert < len(nums) {
nums[insert] = 0
insert++
}
}
复杂度分析
- 时间复杂度:$O(n)$,收集段读指针扫全数组一次,补零段最多再写
n - insert个位置,两段合计每个下标至多被写一次。- 空间复杂度:$O(1)$,只用了
insert一个额外变量,全部操作在原数组上完成。
关键点总结
- 读写双指针适合“稳定保留满足条件的元素”这类原地数组题。
- 正确性的核心是不变量:写指针左侧始终是已扫描区域的非零元素序列。
- 两段式先压缩、后补零,逻辑独立;判断条件必须是
!= 0,负数同样要保留。
易错点总结
- 漏掉补零阶段:
[0,1,0,3]压缩后可能暂时是[1,3,0,3],尾部旧值没有被清除。- 写成
num > 0:[-1,0,2]会丢失-1;题目区分的是零与非零。- 补零从下标 0 开始:会覆盖已经整理好的非零前缀,必须从
insert开始。- 直接排序或交换首尾的零:可能改变非零元素相对顺序,如
[3,1,0,2]不能变成[1,2,3,0]。
相似题目
| 题目 | 难度 | 考察点 |
|---|---|---|
| 26. 删除有序数组中的重复项 | 简单 | 写指针去重,保留条件变为「与前一保留值不同」 |
| 27. 移除元素 | 简单 | 同款模板,剔除目标改为给定值且无需补零 |
| 75. 颜色分类 | 中等 | 三向分区,不再要求稳定,双写指针夹逼 |
| 80. 删除有序数组中的重复项 II | 中等 | 保留条件升级为「至多出现两次」,需回看写指针前两位 |
| 203. 移除链表元素 | 简单 | 同一思想搬到链表,改结点指针代替搬移元素 |
| 443. 压缩字符串 | 中等 | 读写指针进阶,写入内容需现场计算长度 |