LeetCode 283. 移动零:双指针的巧妙运用

LeetCode 283. 移动零:双指针的巧妙运用

_

在处理数组的原地操作(In-place)问题时,“双指针”技巧是我们必须掌握的核心武器。今天我们通过一道非常经典的 LeetCode 简单题——移动零,来深入理解快慢指针的实战应用。

📝 题目描述

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

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

示例 1:

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

输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]

输出: [0]

💡 解题思路:快慢指针

题目的核心要求有两个:

  1. 原地操作:不能 new 一个新数组把非零元素挑出来再补零(空间复杂度必须为 $O(1)$)。

  2. 保持相对顺序:非零元素之间的先后位置不能打乱。

面对这种需求,快慢指针(Fast and Slow Pointers)是完美的解法。

我们可以定义两个指针,都从数组开头 0 索引处出发:

  • 快指针 (fast):像一个不知疲倦的侦察兵,负责遍历整个数组,它的目标是去寻找非零元素

  • 慢指针 (slow):像一个驻守营地的指挥官,它指向的位置,就是下一个非零元素应该存放的位置

工作流程:

当快指针找到一个非零元素时,我们就把它和慢指针所指向的元素进行交换。交换完成后,慢指针向前走一步,准备接收下一个非零元素。如果快指针遇到的是 0,那么它直接跳过继续向前,慢指针留在原地不动。

通过这样的配合,所有非零元素都会被按顺序“挤”到数组的前面,而所有的 0 自然而然就被交换到了数组的末尾。

💻 代码实现

以下是我使用双指针思路编写的 Java 代码,包含了详细的逻辑注释:

package com.okcl;

public class MoveZero {
    public static void main(String[] args) {
        int[] nums = {0, 1, 0, 3, 12};
        
        // 定义快慢指针,初始都指向索引 0
        int fast = 0;
        int slow = 0;
        
        while (fast < nums.length) {
            // 当快指针找到非 0 元素时
            if (nums[fast] != 0) {
                // 判断:如果慢指针此时指向的是 0(或者它俩不相等时)
                // 将快指针的非零元素交换到慢指针的位置
                if (nums[slow] == 0) {
                    int tmp = nums[fast];
                    nums[fast] = nums[slow];
                    nums[slow] = tmp;
                }
                // 此时非零元素已经安放妥当,慢指针前进一步,准备接收下一个非零元素
                slow++;
            }
            // 快指针作为侦察兵,不管遇到什么,每次循环都必须右移继续探索
            fast++;
        }
        
        // 打印结果
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }
}

代码细节探讨:if (nums[slow] == 0) 有必要吗?

在上面的代码中,我加了一个判断 if (nums[slow] == 0),主要是为了更严谨地理解“交换”发生的条件:只有当慢指针卡在 0 的位置时,才需要把后面的非零元素换过来。

但其实这个判断可以省略,直接无条件交换 nums[fast]nums[slow] 也是完全正确的:

  • 情况一:fastslow 还没遇到 0。 它们指向同一个元素(比如开头的 1)。此时自己跟自己交换,没有影响,然后两个指针一起向前走。

  • 情况二:fast 遇到 0 并跳过了,slow 停在 0 上。 此时两者错开,当 fast 遇到非 0 元素时进行交换,正是我们想要的结果。

去掉了内部判断后,代码会变得更加简洁:

Java

public void moveZeroes(int[] nums) {
    int slow = 0;
    for (int fast = 0; fast < nums.length; fast++) {
        if (nums[fast] != 0) {
            // 直接无脑交换
            int temp = nums[fast];
            nums[fast] = nums[slow];
            nums[slow] = temp;
            slow++;
        }
    }
}

📊 复杂度分析

  • 时间复杂度O(N)。由于快指针 fast 只遍历了数组一次,每个元素最多被访问或交换一次,所以时间开销随着数组长度呈线性增长。非常高效。

  • 空间复杂度O(1)。全程只使用了 fastslowtmp 三个整型变量,在原数组上直接操作,没有占用任何额外的数组或集合空间,完美符合题目要求。

📌 总结归纳

通过“快慢指针”,我们可以非常优雅地解决许多数组和链表的“原地修改”或“过滤”问题。快指针负责探路,慢指针负责维持有效数据的边界。 掌握这种一前一后打配合的思维模式,对于提升算法敏锐度非常有帮助。

LeetCode 438. 找到字符串中所有字母异位词:我的解题复盘与实践 2026-05-26
使用 AOP + 自定义注解优雅实现方法级权限校验 2026-05-28

评论区