力扣解题-移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1: 输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
示例 2: 输入: nums = [0] 输出: [0]
提示: 1 <= nums.length <= 104 -231 <= nums[i] <= 231 - 1 进阶:你能尽量减少完成的操作次数吗?
Related Topics 数组 双指针
第一次解答
解题思路
核心方法:分类存储非零/零元素 + 回填原数组,通过两个列表分别存放非零元素和零元素,再按顺序将元素回填到原数组,逻辑简单直观,但存在额外内存开销和冗余操作,性能较差。
具体步骤:
- 初始化两个列表:
numList用于存储数组中的所有非零元素,zeroList用于存储所有零元素,通过列表分类实现“分离非零和零元素”的核心目标。 - 遍历原数组
nums,将每个元素按“是否为0”分类:非零元素加入numList,零元素加入zeroList,完成元素分类。 - 先遍历
numList,将其中的非零元素按原有顺序依次回填到原数组nums的前半部分(下标从0开始),保证非零元素的相对顺序不变。 - 再遍历
zeroList,将所有零元素回填到原数组nums中numList之后的位置,完成“零元素移到末尾”的要求。 - 最终原数组
nums即为处理后的结果。
性能劣势说明
该解法虽然实现了核心功能,但不符合题目“尽量减少操作次数”的进阶要求,且内存和耗时表现差:
- 额外内存开销:创建两个列表存储元素,增加了O(n)的内存消耗,导致内存击败率仅5.09%;
- 冗余操作次数:需要三次遍历(遍历原数组分类+遍历非零列表回填+遍历零列表回填),且涉及列表的增删/访问操作,整体操作次数多,耗时击败率仅9.66%;
- 未充分利用“原地操作”的优化空间,本质是通过额外空间换逻辑简单,而非高效的原地调整。
执行耗时:6 ms,击败了9.66% 的Java用户 内存消耗:47.6 MB,击败了5.09% 的Java用户
public void moveZeroes(int[] nums) {
List<Integer> numList=new ArrayList<>();
List<Integer> zeroList=new ArrayList<>();
for (int num : nums) {
if (num==0){
zeroList.add(num);
}else {
numList.add(num);
}
}
for(int i=0;i<numList.size();i++){
nums[i]=numList.get(i);
}
for(int i=0;i<zeroList.size();i++){
nums[numList.size()+i]=zeroList.get(i);
}
}
第二次解答
解题思路
核心方法:单指针标记非零元素位置 + 原地归位非零元素 + 末尾统一补零,完全基于原数组原地操作,消除额外内存开销,大幅减少操作次数,是更贴合题目进阶要求的高效解法。
具体步骤:
- 初始化两个关键变量:
zeroIndex:作为非零元素的目标占位指针,初始值为0,标记下一个非零元素应存放的位置;zeroNum:统计数组中零元素的个数,用于后续补零操作。
- 遍历原数组
nums的每个元素,核心逻辑是“归位非零元素”:- 若当前元素
nums[i] != 0:将该非零元素赋值到zeroIndex指向的位置(相当于把非零元素按顺序“挤”到数组前面),然后zeroIndex向后移动一位,为下一个非零元素预留位置; - 若当前元素
nums[i] == 0:仅将zeroNum加1,不做其他操作(零元素暂时不处理,留到最后统一补零)。
- 若当前元素
- 遍历完成后,所有非零元素已按原有顺序排列在数组前
nums.length - zeroNum个位置;此时从下标nums.length - zeroNum开始,到数组末尾的位置,统一赋值为0,完成“零元素移到末尾”的要求。 - 全程无额外集合创建,仅在原数组上完成操作,且仅需两次短遍历(一次归位非零元素,一次补零)。
核心优化点说明
- 原地操作,消除内存开销:不再创建额外列表,所有操作基于原数组,内存消耗大幅降低;
- 减少操作次数:仅一次遍历完成所有非零元素的归位,补零阶段仅遍历零元素个数的位置,相比第一次解答的三次遍历,操作次数减少约1/3;
- 无冗余数据拷贝:通过指针直接赋值非零元素,避免列表增删、访问的额外开销,是耗时从6ms降至1ms的核心原因;
- 完全满足题目“保持非零元素相对顺序”和“原地操作”的核心要求,同时贴合“尽量减少操作次数”的进阶目标。
执行耗时:1 ms,击败了99.98% 的Java用户 内存消耗:47.3 MB,击败了5.09% 的Java用户
public void moveZeroes(int[] nums) {
//假定位置0所在的位置
int zeroIndex=0;
int zeroNum=0;
for(int i=0;i<nums.length;i++){
if(nums[i]!=0){
nums[zeroIndex]=nums[i];
zeroIndex++;
}else {
zeroNum++;
}
}
for(int i=nums.length-zeroNum;i<nums.length;i++){
nums[i]=0;
}
}
总结
- 第一次解答通过“列表分类+回填”实现功能,逻辑简单但存在额外内存开销和冗余遍历,性能较差;
- 第二次解答采用“单指针归位非零元素+末尾补零”的原地操作思路,消除额外空间、减少操作次数,是高效且贴合题目要求的最优解;
- 本题的核心优化方向是:放弃“额外空间换简单逻辑”,转而通过指针技巧实现原地操作,减少无效遍历和数据拷贝。