题目描述

✅ 283. 移动零

image-20260928194841924

题意分析

将数组中的所有零移到末尾,保留所有非零元素,并保持非零元素之间原来的相对顺序。数组长度不变,结果直接写回原数组,不创建另一份结果数组。

题目区分的是零与非零,负数也必须保留。只要最终排列满足要求,不必真的把每个零逐个向后搬;进阶还希望减少操作次数,可以着重减少不必要的数组写入。

解法:双指针稳定压缩非零元素

核心思路

[!blue]

把所有零放在末尾,等价于先把非零元素按原顺序紧凑地写到前部,再把剩下的位置补成零。这样无需反复移动一整段元素,也不需要记录每个零的原位置。

用 insert 表示下一个非零元素的写入位置,也等于已经读到的非零元素数量。从左向右读取数组:遇到零就跳过,遇到非零元素就把它放到 nums[insert],然后推进 insert。整个过程中,[0, insert) 始终是已读取部分中的非零元素序列,且顺序与原数组一致。

为什么能一边读、一边修改同一个数组?处理下标 i 时,之前读到的非零元素不会超过 i 个,所以 insert <= i。写入位置只可能是当前下标或已经读过的位置,不会覆盖未来尚未读取的元素。Java 的增强循环和 Go 的 range 都可按这个规律安全读取。

第一遍结束,insert 就是非零元素总数,前缀已经是最终结果。后缀还可能残留旧的非零值,因此必须从 insert 开始检查并补零。全零数组、没有零的数组也按相同流程处理。

为回应进阶要求,两阶段都只在目标位置的值需要改变时赋值:写入非零元素前比较 nums[insert] 与 num,补零前比较当前位置是否已经为零。每个目标位置最多写一次,而且写入的就是最终值;已经正确的位置无需重复写入。即使跳过赋值,insert 仍要照常推进。

解题步骤

  1. 初始化 insert = 0,从左到右读取每个元素 num。
  2. 若 num != 0,仅在 nums[insert] != num 时赋值;随后将 insert 加一,与是否发生赋值无关。
  3. 第一遍结束后,[0, insert) 已包含全部非零元素,且相对顺序不变。
  4. 遍历剩余后缀,仅将其中尚非零的位置改成零,完成原地修改。

代码实现

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. 按奇偶排序数组 简单 同样按条件划分元素,本题要求非零元素相对顺序不变,不能随意使用两端交换。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/93474974
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!