
283. 移动零 - 解题思路整理
题目分析
问题描述:
给定一个整数数组 nums,需要将数组中所有的 0 移动到数组的末尾,同时保持非零元素的相对顺序不变。
核心要求:
-
操作必须原地进行,不能拷贝额外的数组
-
尽量减少操作次数
-
保持非零元素的原始相对顺序
关键约束:
-
时间复杂度 O(n)
-
空间复杂度 O(1)
示例说明:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
输入: [0,0,1]
输出: [1,0,0]
算法分析
1. 双指针划分法(分区维护)
核心思想:
通过两个指针在数组内部划分三个逻辑区间,模拟快速排序的单趟划分过程,将零作为"基准值"进行处理。
指针定义:
-
dest:已处理区间内最后一个非零元素的位置- 初始值:
-1(表示还没有非零元素)
- 初始值:
-
cur:当前遍历位置的指针,用于扫描整个数组- 初始值:
0
- 初始值:
区间划分:
在 cur指针扫描过程中,数组被逻辑上划分为三个部分:
[0, dest] [dest+1, cur-1] [cur, n-1]
┌─────────────┐ ┌───────────────┐ ┌─────────┐
│ 已处理的 │ │ 已处理的 │ │ 待处理 │
│ 非零元素 │ │ 零元素 │ │ 的元素 │
│ (顺序正确) │ │ (顺序正确) │ │ │
└─────────────┘ └───────────────┘ └─────────┘
算法流程:
初始化 dest = -1
for cur 从 0 到 n-1 遍历:
if nums[cur] != 0: # 遇到非零元素
dest = dest + 1 # 扩展非零区
swap(nums[dest], nums[cur]) # 将非零元素交换到非零区末尾
# 遇到0时,cur++即可(0已经在零区的起始位置)
操作细节:
-
遇到0的情况:
-
零元素已经在它应该在的位置(零区的开头)
-
只需
cur++继续扫描,不需要额外操作
-
-
遇到非零的情况:
-
将这个非零元素交换到非零区的末尾(
dest+1位置) -
扩展非零区的边界(
dest++) -
继续扫描(
cur++)
-
算法终止条件:
-
当
cur == n时,所有元素都已处理完成 -
此时
[0, dest]区间包含所有非零元素(保持原序) -
[dest+1, n-1]区间包含所有零元素
2. 算法正确性证明
保持顺序不变:
-
每个非零元素在遇到时,都会被交换到当前非零区的末尾
-
交换操作不会改变已处理非零元素之间的相对顺序
-
因此所有非零元素的相对顺序得以保持
零元素位置正确:
-
零元素不需要主动移动
-
通过与非零元素的交换,零元素自然会被"挤"到数组右侧
-
当算法结束时,所有零元素都在数组末尾
3. 复杂度分析
时间复杂度:O(n)
-
只需一次线性扫描,每个元素最多被访问一次
-
每个元素最多被交换一次
空间复杂度:O(1)
-
只使用了常数级别的额外空间(
dest和cur两个指针) -
满足原地操作的要求
4. 与快速排序的关联
该方法本质上是快速排序中 partition 操作的简化版:
-
将数组元素分为两类:非零元素("小于基准")和零元素("大于等于基准")
-
dest指针维护分区的边界 -
不需要递归调用,一次遍历即可完成
5. 关键点总结
-
分区思想:维护"已处理非零区"和"已处理零区"的边界
-
交换策略:只交换非零元素到正确位置,零元素自然到位
-
顺序保持:通过顺序交换保证非零元素的相对顺序
-
效率优化:一次遍历完成,无冗余操作
这种方法不仅解决了当前问题,其"双指针分区"的思想还可以应用于:
-
- 移除元素
-
- 颜色分类(荷兰国旗问题)
-
-
按奇偶排序数组
等一系列数组原地修改问题
public void moveZeroes2(int[] nums) { for(int cur=0,dest=-1;cur<nums.length;cur++){ if(nums[cur]!=0){ dest++; int temp=nums[cur]; nums[cur]=nums[dest]; nums[dest]=temp; } } }错误写法
你的代码有几个关键问题,我给你逐一分析:
存在的问题:
1. 循环条件错误
for (int i = 0; i < nums.length-1; i++) // 错误 -
你使用
i作为循环变量,但在循环体内却操作cur指针 -
循环条件是
i < nums.length-1,这会漏掉最后一个元素 -
正确的写法是
cur < nums.length或i < nums.length
2. 逻辑混乱,变量使用不当
if (nums[i] == 0) { // 用 i 来判断 cur++; // 但更新的是 cur }-
判断用的是
nums[i],但更新的是cur指针 -
这会导致逻辑错乱,
cur和i可能指向不同位置
3. swap函数错误(Java值传递)
private void swap(int a, int b) { int temp = a; a = b; b = temp; }-
这是最严重的问题 :Java 是值传递,这个
swap函数完全无效! -
交换的只是局部变量的值,不会影响原始数组
-
必须通过数组索引直接交换元素
#### 4. **dest指针越界风险** swap(nums[dest+1], nums[cur]); // 当 dest=-1 时,dest+1=0-
第一次遇到非零元素时,
dest=-1,dest+1=0 -
如果此时
cur=0,就是自己和自己交换 -
虽然不会出错,但逻辑不够清晰
-
统一指针变量 :只使用
cur作为遍历指针,或者用i但保持一致 -
修正循环条件 :
cur < nums.length而不是length-1 -
直接交换数组元素:不能通过普通参数交换,必须在方法内直接操作数组
-
简化逻辑 :你的版本中遇到0时
cur++,遇到非零时也cur++,这可以统一
### 你的代码思路与正确思路对比: 你的思路(有问题): if (nums[i] == 0) { cur++; // 遇到0,cur前进 } else { swap(nums[dest+1], nums[cur]); // 交换 dest++; // 扩展非零区 cur++; // 前进 } 正确思路: if (nums[cur] != 0) { // 遇到非零元素 // 交换到非零区末尾 int temp = nums[++dest]; // 先dest+1,再使用 nums[dest] = nums[cur]; nums[cur] = temp; } // 遇到0什么都不用做,循环会自动cur++ **核心差异**:正确版本中遇到0不需要任何操作,遇到非零时才进行交换操作。 ### 修正后的正确代码: public void moveZeroes(int[] nums) { int dest = -1; // 已处理区间最后一个非零元素的位置 for (int cur = 0; cur < nums.length; cur++) { if (nums[cur] != 0) { // 遇到非零元素 // 将非零元素交换到非零区末尾 dest++; // 先扩展非零区 // 直接交换数组元素,不能通过值传递的swap函数 int temp = nums[dest]; nums[dest] = nums[cur]; nums[cur] = temp; } // 遇到0时,cur++即可(循环会自动cur++) } } ### 更简洁的写法(一步完成交换): public void moveZeroes(int[] nums) { int dest = -1; for (int cur = 0; cur < nums.length; cur++) { if (nums[cur] != 0) { // 交换 nums[++dest] 和 nums[cur] int temp = nums[++dest]; nums[dest] = nums[cur]; nums[cur] = temp; } } } ### 关键修正点总结: public void moveZeroes(int[] nums) { int cur=0; int dest=-1; for (int i = 0; i < nums.length-1; i++) { if (nums[i] == 0) { cur++; }else{ swap(nums[dest+1],nums[cur]); dest++; cur++; } } } private void swap(int a,int b){ int temp=a; a=b; b=temp; } -