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-盛最多水的容器 — 虽然也是双指针,但用的是”对撞指针”(从两端向中间),和快慢指针(同向移动)是双指针的两个不同分支