283 移动零:把数组当成三段区间,用双指针原地"分区+稳定"
题目要求很明确:
- 把所有
0挪到数组末尾 - 非零元素的相对顺序不能变(稳定)
- 必须原地操作,不能额外复制数组
我一看到"原地 + 稳定 + 把某一类元素挪到一边",脑子里就会自动弹出一个关键词:数组分区(partition) 。
不同于快排那种不稳定分区,这题还要求稳定,所以需要一种"稳定分区"的写法。
1. 把问题翻译成"区间维护"
来看这段代码之前,我先给数组划三个区间(这是这题最关键的建模):
[0, dest]:已经整理好的 非零区[dest+1, cur-1]:已经扫过但被挤到后面的 零区[cur, n-1]:还没处理的 待处理区
只要我能保证这三个区间的定义始终成立,那么当 cur 扫完整个数组时:
[0, dest]全是非零,且顺序稳定- 后面自然就是一堆零
这就完成题目要求。
2. 双指针各自扮演什么角色?
这段代码里两个指针:
cur:扫描指针,从左到右看每一个元素dest:非零区的"最后位置"(也可以理解为"下一个非零该放到哪里 - 1")
初始化写成:
cur = 0dest = -1
dest = -1 的好处是:当第一个非零出现时,先 dest++ 就正好落在 0 下标。
3. 核心动作:遇到非零就把它"放到非零区末尾"
来看这段代码的核心部分:
java
if(nums[cur] != 0){
dest++;
int tmp = nums[cur];
nums[cur] = nums[dest];
nums[dest] = tmp;
}
这里的逻辑很像"稳定地把非零元素往前收集":
- 当
nums[cur]是非零:说明它应该进入非零区 dest++:把非零区的边界扩展一格- 交换
nums[cur]和nums[dest]:把这个非零放到非零区末尾
为什么交换不会破坏相对顺序?
关键在于:cur 是从左到右扫的。
每次遇到一个非零元素,它都会被放到 dest 的下一个位置。也就是说:
- 先遇到的非零会先占据更靠前的位置
- 后遇到的非零只会放在更靠后的位置
因此非零元素的相对顺序天然保持不变。
这就满足了"稳定"的要求。
4. 为什么这算"尽量减少操作次数"?
这题进阶问:能不能尽量减少操作?
这份写法的交换次数等于"非零元素的个数"。更准确地说:
- 当
cur == dest时,交换的是自己和自己,其实可以优化掉(属于微优化) - 但即使不优化,时间复杂度还是
O(n),在面试和题解里完全可接受
如果想进一步减少无意义交换,可以加一个判断:
java
if (cur != dest) swap(...)
不过不加也不会影响正确性。
5. 用示例走一遍,区间感会更清楚
数组:[0,1,0,3,12]
- 初始:dest=-1
- cur=0,nums[cur]=0 → 跳过
非零区为空 - cur=1,nums[cur]=1 非零 → dest=0,交换 nums[1] 和 nums[0]
数组变[1,0,0,3,12] - cur=2,nums[cur]=0 → 跳过
- cur=3,nums[cur]=3 非零 → dest=1,交换 nums[3] 和 nums[1]
数组变[1,3,0,0,12] - cur=4,nums[cur]=12 非零 → dest=2,交换 nums[4] 和 nums[2]
数组变[1,3,12,0,0]
结束:非零区 [0..2],零自然在末尾。
6. 完整可用代码(Java)
来看这段代码,就是完整的解法:
java
class Solution {
public void moveZeroes(int[] nums) {
// 数组分组思想:用双指针维护区间
// [0, dest] : 已整理好的非零元素区(稳定)
// [dest+1, cur-1]: 已扫描过的零元素区
// [cur, n-1] : 待处理区
for (int cur = 0, dest = -1; cur < nums.length; cur++) {
if (nums[cur] != 0) {
dest++;
int tmp = nums[cur];
nums[cur] = nums[dest];
nums[dest] = tmp;
}
}
}
}
总结:这题真正想考的"套路"
- 把数组划成区间,维护一个清晰不变量
cur负责扫全数组dest负责标记"非零区的末尾"- 遇到非零就塞到非零区末尾,顺序自然稳定
- 扫完一遍,零就被挤到后面了
这套"稳定分区"思路非常通用:以后遇到"把某类元素挪到一侧、保持相对顺序、原地操作"的题,十有八九都能用类似的双指针区间法解决。