一分钟解决一道高频面试算法题——移动零(双指针)

282 阅读6分钟

前置知识(可跳过)

1.splice() 方法

splice() 方法用于修改数组的内容,可以删除、替换或添加元素。它会直接修改原始数组。

语法:

array.splice(start, deleteCount, item1, item2, ...)

参数:

  • start: 必需。指定修改开始的位置(索引)。

    • 如果 start 超出数组的长度,则从数组末尾开始添加元素。
    • 如果 start 是负数,则从数组末尾开始计算,例如 -1 表示数组的最后一个元素。
  • deleteCount: 可选。指定要删除的元素数量。

    • 如果 deleteCount 为 0,则不删除任何元素。
    • 如果 deleteCount 大于 start 之后的元素总数,则从 start 开始删除所有元素。
    • 如果省略 deleteCount,则从 start 开始删除所有元素。
  • item1, item2, ...: 可选。要添加到数组中的元素。从 start 位置开始插入。

返回值:

splice() 方法返回一个包含被删除元素的数组。如果没有删除任何元素,则返回一个空数组。

2.push() 方法

push() 方法用于将一个或多个元素添加到数组的末尾,并返回修改后的数组的新长度。

语法:

  
array.push(item1, item2, ...);

参数:

  • item1, item2, ...: 要添加到数组末尾的元素。

返回值:

push() 方法返回修改后的数组的新长度。

二、题目描述——移动零

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

进阶: 你能尽量减少完成的操作次数吗?

三、题解

本人错误解法

var moveZeroes = function(nums) {
    let count = 0;
    for(let i=0;i<nums.length;i++){
        if(nums[i]===0){
            nums.splice(i,1);
            count++;
        }
    }
    for(let i=0; i<count;i++){
            nums.push(0)
    }
    return nums;
};

为何错误

1.splice 导致索引错乱:  这是最主要的问题。 当你在循环中使用 splice(i, 1) 删除一个元素后,数组的长度会减小,并且从 i 之后的所有元素的索引都会向前移动一位。 这会导致以下问题:

-   **跳过元素:**  如果 `nums[i]` 是 0, 你删除了它。然后,原来在 `nums[i+1]` 的元素会移动到 `nums[i]`。 但是,循环会直接进入 `i+1` 的下一轮迭代。 这样就跳过了原来 `nums[i+1]` (现在在 `nums[i]` 的位置) 的元素,导致可能没有正确处理它。
-   **越界访问:**  极端情况下,当数组末尾有连续的 0 时, 可能会导致数组越界访问。

2.未完全满足原地操作的要求:  虽然结果是在原数组上修改的,但在循环中使用 splice 操作涉及到数组的重构,严格意义上来说,不算是最高效的原地操作 (虽然题目没有明确禁止使用)。

正确题解一

/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var moveZeroes = function(nums) {
    let insertPos = 0; //指向下一个要插入非零元素的位置

    // 第一次遍历:将所有非零元素移动到数组的前面
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] !== 0) {
            nums[insertPos] = nums[i];
            insertPos++;
        }
    }

    // 第二次遍历:将数组剩余的位置填充为 0
    while (insertPos < nums.length) {
        nums[insertPos] = 0;
        insertPos++;
    }
};

  • 思路

    1. 把所有非零的元素挪到数组的前面,保持它们的相对顺序。
    2. 剩下的位置都用零填充。
  • 具体步骤

    1.第一次遍历(收集非零元素)

    • insertPos 相当于一个指针,它始终指向下一个应该放置非零元素的位置。
    • 当 nums[i] 不是 0 时,把它放到 nums[insertPos] 的位置上。 然后, insertPos 向后移动一位,准备存放下一个非零元素。
    • 循环结束后, insertPos 前面的所有元素都变成了非零元素,并且保持了它们在原数组中的相对顺序。 关键是,insertPos 也记录了非零元素有多少个。

    2.第二次遍历(填充零)

    • 从 insertPos 开始到数组的末尾,把所有元素都设置为 0。 因为第一次遍历后,insertPos 指向的位置以及后面的所有位置,原来都是零或者被非零元素覆盖掉的位置,现在应该填充为零.

正确题解二(最优题解)

function moveZeroes(nums) {
  if (!nums || nums.length === 0) {
    return;
  }

  let left = 0;
  for (let right = 0; right < nums.length; right++) {
    if (nums[right] !== 0) {
      if (left !== right) { // 避免不必要的交换
        nums[left] = nums[right];
        nums[right] = 0; // 直接将 right 位置设为 0
      }
      left++;
    }
  }
}
  • 核心思路

    1. 使用两个指针 left 和 rightleft 指向下一个非零元素应该放置的位置, right 用于遍历数组。
    2. right 指针遍历数组遇到非零元素时,将其移动到 left 指针指向的位置。
    3. 移动后,right 原来的位置设置为0。 这就保证了所有非零元素都在数组的前面,而所有零都在数组的后面。
    4. 关键优化: 避免不必要的交换。 只有当 left 和 right 指针不相等时,才进行交换操作。 这可以减少当非零元素已经在正确位置上时的操作。
  • 具体步骤分解

    1. 初始化

      • left = 0left 指针初始化为 0,表示数组的起始位置,也就是下一个非零元素应该放的位置。
    2. 遍历数组

      • right 指针从数组的起始位置开始,向右移动,遍历整个数组。

      • 如果 nums[right] !== 0 (遇到非零元素)

        • 检查是否需要交换if (left !== right) 。 如果 left 和 right 指向不同的位置,说明 right 指针当前指向的非零元素需要移动到 left 指针指向的位置。
        • 交换元素nums[left] = nums[right]; nums[right] = 0; 将 right 指针指向的非零元素移动到 left 指针指向的位置,并将 right 指针指向的位置设置为 0。 这一步是关键,它保证了零被移动到数组的末尾。
        • left++left 指针向右移动一位,指向下一个非零元素应该放置的位置。
    3. 遍历完成: 当 right 指针遍历完整个数组后,所有非零元素都被移动到了数组的前面,而所有零都被移动到了数组的末尾。

  • 为什么这个解法更优?

    • 一次遍历: 只需要一次遍历即可完成操作,提高了效率。
    • 减少交换次数: 通过 if (left !== right) 的判断,避免了不必要的交换操作,进一步提高了效率。 例如,如果数组前几个元素都是非零元素,left 和 right 会同步前进,此时不需要进行任何交换。
    • 原地修改: 直接在原数组上操作,空间复杂度为 O(1)。
  • 对比前一个解法

    • 前一个解法使用了两次遍历,第一次遍历找到非零元素并移动,第二次遍历填充零。
    • 这个解法只使用一次遍历,通过一次遍历完成元素的移动和零的填充。

实例与展示

image.png

image.png

屏幕截图 2025-04-19 092628.png

image.png

四、结语

再见