力扣解题-移动零

5 阅读5分钟

力扣解题-移动零

给定一个数组 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 数组 双指针


第一次解答

解题思路

核心方法:分类存储非零/零元素 + 回填原数组,通过两个列表分别存放非零元素和零元素,再按顺序将元素回填到原数组,逻辑简单直观,但存在额外内存开销和冗余操作,性能较差。

具体步骤:

  1. 初始化两个列表:numList用于存储数组中的所有非零元素,zeroList用于存储所有零元素,通过列表分类实现“分离非零和零元素”的核心目标。
  2. 遍历原数组nums,将每个元素按“是否为0”分类:非零元素加入numList,零元素加入zeroList,完成元素分类。
  3. 先遍历numList,将其中的非零元素按原有顺序依次回填到原数组nums的前半部分(下标从0开始),保证非零元素的相对顺序不变。
  4. 再遍历zeroList,将所有零元素回填到原数组numsnumList之后的位置,完成“零元素移到末尾”的要求。
  5. 最终原数组nums即为处理后的结果。

性能劣势说明

该解法虽然实现了核心功能,但不符合题目“尽量减少操作次数”的进阶要求,且内存和耗时表现差:

  1. 额外内存开销:创建两个列表存储元素,增加了O(n)的内存消耗,导致内存击败率仅5.09%;
  2. 冗余操作次数:需要三次遍历(遍历原数组分类+遍历非零列表回填+遍历零列表回填),且涉及列表的增删/访问操作,整体操作次数多,耗时击败率仅9.66%;
  3. 未充分利用“原地操作”的优化空间,本质是通过额外空间换逻辑简单,而非高效的原地调整。

执行耗时: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);
        }
    }

第二次解答

解题思路

核心方法:单指针标记非零元素位置 + 原地归位非零元素 + 末尾统一补零,完全基于原数组原地操作,消除额外内存开销,大幅减少操作次数,是更贴合题目进阶要求的高效解法。

具体步骤:

  1. 初始化两个关键变量:
    • zeroIndex:作为非零元素的目标占位指针,初始值为0,标记下一个非零元素应存放的位置;
    • zeroNum:统计数组中零元素的个数,用于后续补零操作。
  2. 遍历原数组nums的每个元素,核心逻辑是“归位非零元素”:
    • 若当前元素nums[i] != 0:将该非零元素赋值到zeroIndex指向的位置(相当于把非零元素按顺序“挤”到数组前面),然后zeroIndex向后移动一位,为下一个非零元素预留位置;
    • 若当前元素nums[i] == 0:仅将zeroNum加1,不做其他操作(零元素暂时不处理,留到最后统一补零)。
  3. 遍历完成后,所有非零元素已按原有顺序排列在数组前nums.length - zeroNum个位置;此时从下标nums.length - zeroNum开始,到数组末尾的位置,统一赋值为0,完成“零元素移到末尾”的要求。
  4. 全程无额外集合创建,仅在原数组上完成操作,且仅需两次短遍历(一次归位非零元素,一次补零)。

核心优化点说明

  1. 原地操作,消除内存开销:不再创建额外列表,所有操作基于原数组,内存消耗大幅降低;
  2. 减少操作次数:仅一次遍历完成所有非零元素的归位,补零阶段仅遍历零元素个数的位置,相比第一次解答的三次遍历,操作次数减少约1/3;
  3. 无冗余数据拷贝:通过指针直接赋值非零元素,避免列表增删、访问的额外开销,是耗时从6ms降至1ms的核心原因;
  4. 完全满足题目“保持非零元素相对顺序”和“原地操作”的核心要求,同时贴合“尽量减少操作次数”的进阶目标。

执行耗时: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;
        }
    }

总结

  1. 第一次解答通过“列表分类+回填”实现功能,逻辑简单但存在额外内存开销和冗余遍历,性能较差;
  2. 第二次解答采用“单指针归位非零元素+末尾补零”的原地操作思路,消除额外空间、减少操作次数,是高效且贴合题目要求的最优解;
  3. 本题的核心优化方向是:放弃“额外空间换简单逻辑”,转而通过指针技巧实现原地操作,减少无效遍历和数据拷贝