283. 移动零(Move Zeroes)

频次 ★★★ · 难度 🟢 · 高频:美团/快手

题目

给定一个数组 nums,将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。必须原地操作。

示例

输入: nums = [0, 1, 0, 3, 12]
输出: nums 变为 [1, 3, 12, 0, 0]

思路

快慢指针

  • slow 指向下一个非零元素应放的位置
  • fast 遍历数组
  • nums[fast] != 0 时,放到 slow 位置
  • 最后将 slow 之后的位置全部补 0

也可以交换法:非零元素和 slow 位置交换(当 slow != fast 时),这样只需一趟。

代码

public void moveZeroes(int[] nums) {
    int slow = 0;
    for (int fast = 0; fast < nums.length; fast++) {
        if (nums[fast] != 0) {
            if (slow != fast) {
                int temp = nums[slow];
                nums[slow] = nums[fast];
                nums[fast] = temp;
            }
            slow++;
        }
    }
}

复杂度

  • 时间:O(n) — 一次遍历
  • 空间:O(1) — 原地操作

边界条件

  • 数组全为 0:slow 永远不递增,每次遇到非零都不会触发(其实数组里没有非零元素,循环体内的 if 永远不进入),数组保持不变,符合预期(全 0 数组移动 0 后还是全 0)。
  • 数组不含 0:每次 slow == fast,交换是自己和自己交换(无实际影响),等价于原样返回。
  • 只有一个元素:循环执行一次,无论是否为 0 结果都正确。

变式

  • 27. 移除元素 是本题的”一般化版本”:把”过滤掉的元素放到末尾”换成”过滤掉的元素直接丢弃”,slow/fast 骨架完全一样。
  • 如果要求统计移动了多少次:在 slow != fast 触发交换时计数即可,不影响整体复杂度。

易错点

  • 用交换而不是直接赋值(nums[slow] = nums[fast])的原因:直接赋值会覆盖掉 slow 位置原本的值(如果它是非零的会丢失),交换能保证被移走的 0 仍然留在数组里,只是换了位置,不需要额外一趟”把末尾补 0”。
  • slow != fast 时才交换是一个小优化(避免无意义的自交换),但即使去掉这个判断,逻辑依然正确,只是会有一些无效的自我赋值。

面试追问

  • 能不能不用交换,用两趟遍历实现? 可以:第一趟和 27. 移除元素 一样把非零元素依次搬到前面,第二趟把 slow 之后的位置全部置 0;这种写法更直观,但比交换法多了一趟遍历(虽然渐进复杂度仍是 O(n))。
  • 这类”双指针原地过滤并保持顺序”的题目有什么通用规律? 都是同一个模板:slow 指向”下一个满足条件的元素应该存放的位置”,fast 负责探路;区别只在于”满足条件”的判断条件,以及”不满足条件的元素”是丢弃还是要挪到别处保留。

关联题