力扣283题"移动零"是一个经典的数组操作问题。题目要求我们原地修改输入数组,将所有0移动到数组末尾,同时保持非零元素的原始相对顺序。这意味着我们不能使用额外的数组空间,必须在原数组上进行操作。
举个例子:
很多初学者可能会想到以下两种错误解法:
java复制// 错误解法1:创建新数组
public void moveZeroes(int[] nums) {
int[] result = new int[nums.length];
int index = 0;
for (int num : nums) {
if (num != 0) {
result[index++] = num;
}
}
// 这违反了原地修改的要求
System.arraycopy(result, 0, nums, 0, nums.length);
}
java复制// 错误解法2:删除后补零
public void moveZeroes(int[] nums) {
int i = 0;
while (i < nums.length) {
if (nums[i] == 0) {
// 删除操作需要移动大量元素,效率低
for (int j = i; j < nums.length - 1; j++) {
nums[j] = nums[j + 1];
}
nums[nums.length - 1] = 0;
} else {
i++;
}
}
}
这两种解法要么违反了原地修改的要求,要么时间复杂度太高(O(n²)),都不是最优解。
双指针算法是解决这类数组原地操作问题的利器。我们可以想象有两个指针在数组上移动:
关键点:快指针负责探索,慢指针负责构建最终结果。这种分工使得我们可以在一次遍历中完成所有操作。
java复制class Solution {
public void moveZeroes(int[] nums) {
// l指针记录非零元素应该放置的位置
int l = 0;
// r指针遍历整个数组
for (int r = 0; r < nums.length; r++) {
// 发现非零元素
if (nums[r] != 0) {
// 交换l和r位置的元素
swap(nums, l, r);
// l指针右移,准备接收下一个非零元素
l++;
}
}
}
// 辅助交换方法
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
让我们通过一个具体例子来理解算法的执行过程。以输入数组[0,1,0,3,12]为例:
| 步骤 | l | r | nums[r] | 操作 | 数组状态 |
|---|---|---|---|---|---|
| 初始 | 0 | 0 | 0 | r++ | [0,1,0,3,12] |
| 1 | 0 | 1 | 1 | 交换l=0和r=1,l++ | [1,0,0,3,12] |
| 2 | 1 | 2 | 0 | r++ | [1,0,0,3,12] |
| 3 | 1 | 3 | 3 | 交换l=1和r=3,l++ | [1,3,0,0,12] |
| 4 | 2 | 4 | 12 | 交换l=2和r=4,l++ | [1,3,12,0,0] |
| 结束 | 3 | 5 | - | 循环结束 | [1,3,12,0,0] |
原算法中即使非零元素已经在正确位置也会进行交换。我们可以添加一个判断来避免不必要的交换:
java复制public void moveZeroes(int[] nums) {
int l = 0;
for (int r = 0; r < nums.length; r++) {
if (nums[r] != 0) {
// 只有当l和r不同时才需要交换
if (l != r) {
nums[l] = nums[r];
nums[r] = 0;
}
l++;
}
}
}
有些开发者喜欢先移动非零元素,最后统一补零:
java复制public void moveZeroes(int[] nums) {
int l = 0;
// 先将所有非零元素移到前面
for (int r = 0; r < nums.length; r++) {
if (nums[r] != 0) {
nums[l++] = nums[r];
}
}
// 将剩余位置补零
while (l < nums.length) {
nums[l++] = 0;
}
}
这种写法虽然需要两次遍历,但交换操作更少,在某些情况下可能更高效。
有初学者可能会想直接覆盖而非交换:
java复制// 错误写法
nums[l] = nums[r];
nums[r] = 0;
这在大多数情况下确实能工作,但当l和r指向同一个元素时(数组开头就是非零元素),会导致非零元素被错误地置零。
双指针算法天然能处理以下边界条件:
快指针的任务是探索整个数组,寻找非零元素。无论当前元素是否为0,快指针都需要继续前进,这样才能确保不遗漏任何元素。
双指针算法不仅适用于移动零问题,还广泛应用于以下场景:
掌握双指针技巧的关键在于理解两个指针的分工和移动条件。在实际编程中,我建议先用具体例子手动模拟指针移动过程,再转化为代码,这样可以减少逻辑错误。