Skip to content

实战:移动零

这道题是双指针的另一个经典应用。与上一题"删除重复项"不同的是,这里我们需要交换元素,而不是简单地覆盖。

题目描述

LeetCode 283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意,必须在不复制数组的情况下原地对数组进行操作。

示例 1

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

示例 2

输入:nums = [0]
输出:[0]

题目分析

关键点:

  1. 原地修改:不能创建新数组
  2. 保持相对顺序:非零元素的先后顺序不能变
  3. 零移到末尾:所有 0 都要在数组最后

思考一下:如果不要求保持相对顺序,怎么做?

可以用首尾双指针,左指针找 0,右指针找非零,交换它们。但这会打乱非零元素的顺序。

要保持相对顺序,就要用快慢指针,从同一方向遍历。

解法一:双指针 + 交换

核心思路:

  • slow 指针:指向下一个非零元素应该放置的位置
  • fast 指针:遍历数组,寻找非零元素
  • 遇到非零元素,就和 slow 位置的元素交换
javascript
function moveZeroes(nums) {
    let slow = 0;
    
    for (let fast = 0; fast < nums.length; fast++) {
        if (nums[fast] !== 0) {
            // 交换 slow 和 fast 位置的元素
            [nums[slow], nums[fast]] = [nums[fast], nums[slow]];
            slow++;
        }
    }
}

执行过程

nums = [0, 1, 0, 3, 12] 为例:

初始:slow = 0, fast = 0
[0, 1, 0, 3, 12]

 s,f

fast=0: nums[0]=0, 是零 → 跳过

fast=1: nums[1]=1, 非零 → 交换 nums[0] ↔ nums[1]
[1, 0, 0, 3, 12], slow = 1
 ↑  ↑
 s  f

fast=2: nums[2]=0, 是零 → 跳过

fast=3: nums[3]=3, 非零 → 交换 nums[1] ↔ nums[3]
[1, 3, 0, 0, 12], slow = 2
    ↑     ↑
    s     f

fast=4: nums[4]=12, 非零 → 交换 nums[2] ↔ nums[4]
[1, 3, 12, 0, 0], slow = 3
       ↑      ↑
       s      f

结果:[1, 3, 12, 0, 0] ✓

为什么能保持相对顺序?

因为 fast 是从左到右遍历的,非零元素按原来的顺序被移到前面。

复杂度分析

  • 时间复杂度:O(n)——遍历一次数组
  • 空间复杂度:O(1)——只用了两个指针变量

优化:避免自己和自己交换

slow === fast 时,交换自己和自己没有意义。可以加个判断:

javascript
function moveZeroes(nums) {
    let slow = 0;
    
    for (let fast = 0; fast < nums.length; fast++) {
        if (nums[fast] !== 0) {
            if (slow !== fast) {
                [nums[slow], nums[fast]] = [nums[fast], nums[slow]];
            }
            slow++;
        }
    }
}

这个优化在数组开头没有零的情况下可以减少一些操作,但整体复杂度不变。

解法二:覆盖 + 补零

另一种思路:先把所有非零元素移到前面,然后把剩余位置填零。

javascript
function moveZeroes(nums) {
    let slow = 0;
    
    // 第一遍:把非零元素移到前面
    for (let fast = 0; fast < nums.length; fast++) {
        if (nums[fast] !== 0) {
            nums[slow] = nums[fast];
            slow++;
        }
    }
    
    // 第二遍:剩余位置填零
    while (slow < nums.length) {
        nums[slow] = 0;
        slow++;
    }
}

执行过程:

[0, 1, 0, 3, 12]

第一遍:移动非零元素
fast=0: 0, 跳过
fast=1: 1, nums[0]=1, slow=1
fast=2: 0, 跳过
fast=3: 3, nums[1]=3, slow=2
fast=4: 12, nums[2]=12, slow=3

数组变成:[1, 3, 12, 3, 12]

                  slow=3

第二遍:填零
nums[3]=0, nums[4]=0

结果:[1, 3, 12, 0, 0] ✓

复杂度分析

  • 时间复杂度:O(n)——两次遍历,总共 O(2n) = O(n)
  • 空间复杂度:O(1)

两种解法对比

方法遍历次数操作类型特点
交换法1 次交换代码简洁
覆盖法2 次覆盖 + 填充逻辑清晰

两种方法的时间复杂度都是 O(n),空间复杂度都是 O(1)。

在实际中,交换法可能稍快一点,因为只遍历一次。但覆盖法的逻辑更直观。

选择哪种取决于个人偏好。

常见错误

  1. 没有保持相对顺序:使用首尾双指针会打乱顺序
  2. 直接删除零:用 splice 删除元素会导致 O(n²) 复杂度

相关题目

  • 27. 移除元素:移除所有等于 val 的元素(与本题思路相同)
  • 26. 删除有序数组中的重复项:上一章学过

本章小结

这一章我们又练习了一次快慢双指针

  1. 快指针:遍历数组,找非零元素
  2. 慢指针:记录下一个非零元素应该放的位置
  3. 两种实现:交换法(一次遍历)和覆盖法(两次遍历)

双指针的核心思想是:用一个指针读取,用另一个指针写入

这个模式会反复出现,一定要熟练掌握。

下一章,我们来看一道需要更多技巧的题目:「合并两个有序数组」。

实战:移动零 has loaded