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

题意分析
将数组中的所有零移到末尾,保留所有非零元素,并保持非零元素之间原来的相对顺序。数组长度不变,结果直接写回原数组,不创建另一份结果数组。
题目区分的是零与非零,负数也必须保留。只要最终排列满足要求,不必真的把每个零逐个向后搬;进阶还希望减少操作次数,可以着重减少不必要的数组写入。
解法:双指针稳定压缩非零元素
核心思路
[!blue]
把所有零放在末尾,等价于先把非零元素按原顺序紧凑地写到前部,再把剩下的位置补成零。这样无需反复移动一整段元素,也不需要记录每个零的原位置。
用
insert表示下一个非零元素的写入位置,也等于已经读到的非零元素数量。从左向右读取数组:遇到零就跳过,遇到非零元素就把它放到nums[insert],然后推进insert。整个过程中,[0, insert)始终是已读取部分中的非零元素序列,且顺序与原数组一致。为什么能一边读、一边修改同一个数组?处理下标
i时,之前读到的非零元素不会超过i个,所以insert <= i。写入位置只可能是当前下标或已经读过的位置,不会覆盖未来尚未读取的元素。Java 的增强循环和 Go 的range都可按这个规律安全读取。第一遍结束,
insert就是非零元素总数,前缀已经是最终结果。后缀还可能残留旧的非零值,因此必须从insert开始检查并补零。全零数组、没有零的数组也按相同流程处理。为回应进阶要求,两阶段都只在目标位置的值需要改变时赋值:写入非零元素前比较
nums[insert]与num,补零前比较当前位置是否已经为零。每个目标位置最多写一次,而且写入的就是最终值;已经正确的位置无需重复写入。即使跳过赋值,insert仍要照常推进。
解题步骤
- 初始化
insert = 0,从左到右读取每个元素num。- 若
num != 0,仅在nums[insert] != num时赋值;随后将insert加一,与是否发生赋值无关。- 第一遍结束后,
[0, insert)已包含全部非零元素,且相对顺序不变。- 遍历剩余后缀,仅将其中尚非零的位置改成零,完成原地修改。
代码实现
class Solution {
public void moveZeroes(int[] nums) {
int insert = 0;
for (int num : nums) {
if (num != 0) {
// insert 左侧始终保存已压缩的非零元素。
if (nums[insert] != num) {
nums[insert] = num;
}
insert++;
}
}
while (insert < nums.length) {
if (nums[insert] != 0) {
nums[insert] = 0;
}
insert++;
}
}
}
func moveZeroes(nums []int) {
insert := 0
for _, num := range nums {
if num != 0 {
// 非零元素按原顺序写到数组前部。
if nums[insert] != num {
nums[insert] = num
}
insert++
}
}
for insert < len(nums) {
if nums[insert] != 0 {
nums[insert] = 0
}
insert++
}
}
复杂度分析
- 时间复杂度:$O(n)$,第一遍读取整个数组,第二遍检查长度不超过
n的后缀。条件判断减少的是数组写入次数,不改变线性时间复杂度。- 空间复杂度:$O(1)$,只维护写入位置和当前元素,全部修改都发生在原数组中。
关键点总结
[!green]
- 读写双指针适合“稳定保留满足条件的元素”这类原地数组题。
- 正确性的核心是不变量:写指针左侧始终是已扫描区域的非零元素序列。
- 先确定非零前缀,再处理剩余后缀;未变动的位置不需要写入,但每个非零元素仍要占用一个结果位置。
易错点总结
[!yellow]
- 漏掉补零阶段,尾部可能残留压缩前的非零元素;补零也不能从下标
0开始,否则会覆盖已经整理好的前缀。- 保留条件写成
num > 0会丢失负数,应判断num != 0。- 只在发生赋值时推进
insert,会漏算原本就在正确位置的非零元素,导致后续覆盖或补零出错。- 直接排序或随意用尾部非零值替换前面的零,会破坏非零元素的相对顺序;写入必须遵循从左向右的读取顺序。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 27. 移除元素 | 简单 | 同样把需要保留的元素稳定压到前面,本题还需把尾部全部补成0并保留数组长度。 |
| 905. 按奇偶排序数组 | 简单 | 同样按条件划分元素,本题要求非零元素相对顺序不变,不能随意使用两端交换。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!