问题描述
给定一个整数数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
要求:
-
必须在原数组上操作,不能拷贝额外的数组
-
尽量减少操作次数
思路:双指针交换法
算法思想
这道题的难点在于如何在不使用额外空间的情况下,原地完成零的移动,并且保持非零元素的顺序。双指针交换法巧妙地解决了这个问题。
我们使用两个指针:
-
慢指针(left):指向下一个非零元素应该放置的位置
-
快指针(right) :遍历整个数组,寻找非零元素
算法步骤
-
初始化两个指针都指向数组开头
-
快指针遍历整个数组
-
当快指针遇到非零元素时,与慢指针指向的元素交换
-
交换后,慢指针向前移动一位
-
继续遍历直到数组结束
Java实现代码
java
class Solution {
public void moveZeroes(int[] nums) {
int left = 0; // 慢指针
// 快指针遍历数组
for (int right = 0; right < nums.length; right++) {
// 找到非零元素
if (nums[right] != 0) {
// 交换非零元素到前面
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++; // 慢指针右移
}
}
}
}
逐步解析执行过程
让我们以示例 [0, 1, 0, 3, 12] 为例,一步步解析代码的执行:
初始状态
text
数组: [0, 1, 0, 3, 12]
指针: left=0, right=0
第1步:right=0
text
nums[0] = 0,不满足条件,不交换
数组: [0, 1, 0, 3, 12]
指针: left=0, right=1
第2步:right=1
text
nums[1] = 1 ≠ 0,满足条件
交换 nums[0] 和 nums[1]
交换后:left=0 → nums[0]=1, right=1 → nums[1]=0
数组: [1, 0, 0, 3, 12]
指针: left=1, right=2
第3步:right=2
text
nums[2] = 0,不满足条件,不交换
数组: [1, 0, 0, 3, 12]
指针: left=1, right=3
第4步:right=3
text
nums[3] = 3 ≠ 0,满足条件
交换 nums[1] 和 nums[3]
交换后:left=1 → nums[1]=3, right=3 → nums[3]=0
数组: [1, 3, 0, 0, 12]
指针: left=2, right=4
第5步:right=4
text
nums[4] = 12 ≠ 0,满足条件
交换 nums[2] 和 nums[4]
交换后:left=2 → nums[2]=12, right=4 → nums[4]=0
数组: [1, 3, 12, 0, 0]
指针: left=3, right=5
最终结果
遍历结束,得到最终结果:[1, 3, 12, 0, 0]
算法复杂度分析
时间复杂度:O(n)
-
我们只需要遍历数组一次,快指针从0到n-1,总共n次操作
-
每次操作都是常数时间
空间复杂度:O(1)
-
只使用了常数个额外变量(left, right, temp)
-
真正实现了原地算法
算法优化
虽然上述代码已经非常高效,但我们可以做一个小优化:避免不必要的自交换。
优化版本
java
class Solution {
public void moveZeroes(int[] nums) {
int left = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] != 0) {
// 只有当两个指针位置不同时才需要交换
if (left != right) {
nums[left] = nums[right];
nums[right] = 0;
}
left++;
}
}
}
}
优化点:
-
当
left == right时,说明当前位置已经正确,不需要交换 -
直接赋值而非交换,在某些情况下可以减少操作次数
双指针法的精妙
1. 分区思想
双指针法实际上实现了数组的分区:
-
[0, left):已处理的非零元素 -
[left, right):已处理的零元素(如果有) -
[right, n):待处理的元素
2. 保持顺序的机制
通过交换而非直接覆盖,我们确保了非零元素的相对顺序不会被打乱。每次交换都是将当前的非零元素与最前面的零交换。
3. 高效性
只需一次遍历就完成了所有操作,没有冗余的移动或比较。
实际应用场景
这种双指针技巧不仅适用于移动零问题,还可以解决多种类似问题:
-
删除排序数组中的重复项(LeetCode 26)
-
移除元素(LeetCode 27)
-
颜色分类(LeetCode 75,荷兰国旗问题)
-
快速排序的分区操作
总结
双指针交换法是解决"移动零"问题的经典且最优解法。它的核心思想是:
-
使用两个指针区分已处理和未处理的区域
-
通过交换将非零元素移动到前面
-
在一次遍历中完成所有操作
这种解法的优点非常明显:
-
高效:时间复杂度O(n),空间复杂度O(1)
-
简洁:代码量少,逻辑清晰
-
通用:可以推广到其他类似问题
希望这篇解析能帮助你深入理解双指针技巧,并在实际问题中灵活应用。掌握这种核心算法思想,对于提高编程能力和解决复杂问题都有重要意义。