在处理数组的原地操作(In-place)问题时,“双指针”技巧是我们必须掌握的核心武器。今天我们通过一道非常经典的 LeetCode 简单题——移动零,来深入理解快慢指针的实战应用。
📝 题目描述
题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意:必须在不复制数组的情况下 原地 对数组进行操作。
示例 1:
输入:
nums = [0,1,0,3,12]输出:
[1,3,12,0,0]
示例 2:
输入:
nums = [0]输出:
[0]
💡 解题思路:快慢指针
题目的核心要求有两个:
原地操作:不能
new一个新数组把非零元素挑出来再补零(空间复杂度必须为 $O(1)$)。保持相对顺序:非零元素之间的先后位置不能打乱。
面对这种需求,快慢指针(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] 也是完全正确的:
情况一:
fast和slow还没遇到 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)。全程只使用了
fast、slow和tmp三个整型变量,在原数组上直接操作,没有占用任何额外的数组或集合空间,完美符合题目要求。
📌 总结归纳
通过“快慢指针”,我们可以非常优雅地解决许多数组和链表的“原地修改”或“过滤”问题。快指针负责探路,慢指针负责维持有效数据的边界。 掌握这种一前一后打配合的思维模式,对于提升算法敏锐度非常有帮助。