目录

题目描述

283. 移动零

image-20230304223011804

题意分析

给定整数数组 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. 压缩字符串 中等 读写指针进阶,写入内容需现场计算长度