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负责探路;区别只在于”满足条件”的判断条件,以及”不满足条件的元素”是丢弃还是要挪到别处保留。
关联题
- 同套路:27. 移除元素、26. 删除有序数组中的重复项 —— 快慢双指针原地压缩三件套
- 进阶:75. 颜色分类 —— 升级为三指针分区(荷兰国旗问题)
- 知识点:慢指针始终是”已处理区边界”的循环不变量,双指针专题见双指针