283. 移动零 (Easy)
专题归类: 02-双指针与滑动窗口 LeetCode 链接: https://leetcode.cn/problems/move-zeroes/
在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode
题目描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
要求:
- 必须原地操作,不能拷贝额外的数组
- 尽量减少操作次数
示例:
输入: nums = [0, 1, 0, 3, 12]
输出: [1, 3, 12, 0, 0]
补充说明:
- 数组长度范围:
1 <= nums.length <= 10^4 - 数值范围:
-2^31 <= nums[i] <= 2^31 - 1
题目详细分析
数据范围含义:
- 数组最长 10^4,O(n) 完全足够。甚至 O(n^2) 也能过,但不优雅。
- 数值包含正数、负数和零,但题目只说”将 0 移动到最后”,非零元素的相对顺序要保持。
核心约束:
- 原地操作是硬性要求,不能 new 一个新数组再拷贝回去。
- 保持相对顺序——这意味着不能简单地”把所有 0 删了再在后面补 0”,因为删除操作本身需要 O(n) 并且可能改变相对顺序。
边界条件:
- 数组全为非零:不做任何移动,原数组不变。
- 数组全为零:所有元素已经是 0,不需要移动。
- 数组为空:虽然题目说长度 >= 1,但理论上空数组直接返回。
隐藏条件:
- “尽量减少操作次数”暗示了最优解法应该在一次遍历中完成。
- 非零元素的相对顺序不变——这意味着不能用”从两端交换”的方式(如快速排序的 partition),因为那样会打乱相对顺序。
- 目标不是”删除零”,而是”把非零元素挤到前面去”。
小白版直白理解
想象你在整理一排书架上的书。有些位置是空的(就是 0),你要把所有空位挪到最右边,书(非零元素)挤到最左边,而且书的顺序不能变。
笨办法: 你一本一本看,遇到空位就把后面的所有书往前挪一格。这样最坏情况下每本书都要挪很多次。
聪明办法(快慢指针): 你找两个人帮忙——一个人(快指针)在前面跑,把书的位置报给你;另一个人(慢指针)跟着,负责放书。快指针喊”有书!“,你就让慢指针把书放到他当前位置,然后慢指针走一步。快指针喊”空的!“,你就忽略,继续往前走。这样一轮下来,所有书都挤到前面了,空位自然落到了后面。
解题思路
思路一:快慢指针交换法(推荐)
核心想法: 用两个指针(slow 和 fast)协同完成一次遍历中的”筛选+重排”。
关键洞察:
slow指针指向”下一个非零元素应该被放置的位置”。fast指针在前面探路,寻找非零元素。- 当
fast找到非零元素时,将其与slow位置的元素交换,然后slow++。
这样就等价于:非零元素被”交换”到前面,零被”交换”到后面。且因为 slow 始终指向第一个可能为 0 的位置,所有非零元素的相对顺序保持不变。
def moveZeroes(nums):
"""
快慢指针交换法
slow: 下一个非零元素的位置
fast: 遍历数组,寻找非零元素
"""
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
# 将非零元素交换到前面
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1思路二:覆盖后补零法
核心想法: 把非零元素全部提取到前面,剩下的位置全部填 0。
虽然看起来更直观——先迁移非零元素,再补零。每次赋值代替交换,当非零元素占多数时,赋值操作比交换操作更少。
def moveZeroes(nums):
"""
覆盖后补零法
先覆盖非零元素,再补零
"""
# 第一遍:将所有非零元素依次放到前面
pos = 0
for num in nums:
if num != 0:
nums[pos] = num
pos += 1
# 第二遍:剩余位置全部填 0
while pos < len(nums):
nums[pos] = 0
pos += 1思路三:一次遍历直接补零
思路二的优化版:一次遍历同时完成覆盖和补零。
def moveZeroes(nums):
"""
一次遍历直接补零
"""
j = 0
for i in range(len(nums)):
if nums[i] != 0:
nums[j] = nums[i]
if i != j: # 当 i != j 时,将原位置置零
nums[i] = 0
j += 1易错点
- 交换 vs 赋值: 快慢指针交换法中,
nums[slow]和nums[fast]交换,不要写成赋值。如果直接赋值不交换,会导致非零元素被覆盖丢失。 - slow 没有重置: slow 从 0 开始,只增不减。不要在循环内重置。
- 全非零数组的优化: 如果数组没有 0,方法一的每次
swap是自身交换(浪费性能但结果正确),方法三通过if i != j避免了自身赋值。 - 保持顺序: 不能用从两端往中间靠拢的双指针(如快速排序 partition 的交换方式),那样会打乱非零元素的相对顺序。
- 元素不只有 0 和非零: 数值包含负数,但题目只关心是否为 0,负数也属于”非零”,要保留前面的相对顺序。
框架提炼
快慢指针筛选模板:
核心模式:用一个快指针遍历数组,一个慢指针指向”下一个有效位置”,高效筛选特定元素。
def filter_array(nums):
"""
快慢指针通用模板
将满足条件的元素筛选到数组前面
"""
slow = 0 # 下一个有效位置
for fast in range(len(nums)):
if condition(nums[fast]): # 满足筛选条件
# 方式一:交换(保留所有元素到正确位置)
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
# 或方式二:覆盖(只关心有效元素的顺序)
# nums[slow] = nums[fast]
# slow += 1适用场景特征:
- 需要原地重排数组
- 将满足/不满足条件的元素分开
- 保持相对顺序
典型应用:
| 问题 | 筛选条件 | 操作 |
|---|---|---|
| 移动零 | num != 0 | 非零交换到前面 |
| 移除元素 | num != val | 保留不等于 val 的元素 |
| 删除排序数组中的重复项 | 首次出现或与前一个不同 | 保留唯一元素 |
关联题目
- 26-删除有序数组中的重复项 — 同样是快慢指针模板,但条件变为”保留第一个出现的元素”,后续重复的跳过
- 27-移除元素 — 同样是快慢指针,条件变为”保留不等于 val 的元素”,核心思路完全一致
- 11-盛最多水的容器 — 虽然也是双指针,但用的是”对撞指针”(从两端向中间),和快慢指针(同向移动)是双指针的两个不同分支