题目背景
在 LeetCode 283 题「移动零」中,给定一个数组 nums,要求将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。关键约束是必须原地操作,不能复制数组。
示例:
- 输入:
[0,1,0,3,12] - 输出:
[1,3,12,0,0]
双指针核心思路
处理这类'原地重排'问题的经典方法是使用双指针。我们可以将指针分为两个角色:
- 遍历指针 (
cur):负责扫描整个数组,寻找符合条件的元素(非零数)。 - 维护指针 (
dest):指向当前已整理好的非零序列的末尾位置。
具体逻辑如下:
- 初始化
dest = -1,表示尚未发现任何非零元素。 cur从头开始遍历。- 当
nums[cur]不为 0 时,说明找到了一个需要保留的元素。此时先将dest向前移动一位,然后将nums[cur]与nums[dest]交换。 - 如果
dest恰好等于cur,交换相当于自身赋值,不影响结果;如果不等,则实现了将非零元素'冒泡'到左侧的效果。
这种写法避免了额外的空间开销,时间复杂度为 O(n),空间复杂度为 O(1)。
拓展:三指针与分区思想
双指针的本质是对数组进行分区。当我们面对更复杂的条件时,可以引入第三个指针来维护中间区域,这就是三指针原理的典型应用。
以 LeetCode 75 题「颜色分类」为例,需要将数组按 0、1、2 排序。这可以通过类似快速排序的分区思想实现:
- 左块:小于基准值(如 0)
- 中块:等于基准值
- 右块:大于基准值
通过三个指针分别维护这三块的边界,可以在一次遍历内完成排序。这种分区策略不仅适用于颜色分类,也是快速选择算法(Quick Select)和快速排序(Quick Sort)的基础。虽然这些算法的具体实现细节有所不同,但核心都在于利用指针高效地划分数据范围。
代码实现
下面是基于上述思路的 Java 实现。代码中保留了详细的注释,方便理解每一步的意图。
public void moveZeroes(int[] nums) {
int cur = 0; // 遍历指针,扫描整个数组
int dest = -1; // 维护指针,记录最后一个非零元素的位置
while (cur < nums.length) {
if (nums[cur] != 0) {
// 找到非零元素,先移动维护指针
dest++;
nums[dest];
nums[dest] = nums[cur];
nums[cur] = tmp;
}
cur++;
}
}


