一次遍历搞定!用双指针优雅解决 移动零 问题

61 阅读3分钟

题目:给定一个数组 nums,编写一个函数将所有 0 移动到数组末尾,同时保持非零元素的相对顺序。要求:原地操作,不复制数组

题目283. 移动零 - 力扣(LeetCode)

这道题看似简单,但要在满足“原地”和“保序”的前提下高效完成,却藏着对指针思维的巧妙考验。今天,我们就用 真正的双指针交换法,一次遍历、O(1) 空间,优雅破局!


❌ 常见误区:为什么不能直接删 0 再补?

很多初学者会想到:

// 错误示范!
for (let i = 0; i < nums.length; i++) {
  if (nums[i] === 0) {
    nums.splice(i, 1); // 删除 0
    nums.push(0);      // 末尾补 0
  }
}

但这样做有两个致命问题:

  1. splice 是 O(n) 操作,整体复杂度退化为 O(n²);
  2. 修改数组长度会导致索引错乱,可能漏掉连续的 0。

更重要的是——题目明确要求“原地操作”,隐含期望是 O(n) 时间 + O(1) 空间


正解:双指针交换法(一次遍历)

🧠 核心思想

我们维护两个指针:

  • left:始终指向当前第一个 0 的位置(即下一个非零元素应该放的位置);
  • right:从左到右扫描整个数组,寻找非零元素。

right 遇到非零数时,立即与 left 位置交换,然后 left++
这样,所有非零数按顺序被“搬”到前面,0 自然被“挤”到后面。

💡 关键洞察:left 左侧全是非零数,leftright 之间全是 0


🖼️ 图解过程

nums = [0, 1, 0, 3, 12] 为例:

步骤rightnums[right]操作数组状态left
初始---[0,1,0,3,12]0
100跳过[0,1,0,3,12]0
211 ≠ 0交换 0↔1[1,0,0,3,12]1
320跳过[1,0,0,3,12]1
433 ≠ 0交换 0↔3[1,3,0,0,12]2
5412 ≠ 0交换 0↔12[1,3,12,0,0]3

✅ 最终结果:[1, 3, 12, 0, 0] —— 顺序不变,0 全在末尾!


💻 代码实现(JavaScript)

function moveZeroes(nums) {
  let left = 0; // 指向第一个 0 的位置
  
  for (let right = 0; right < nums.length; right++) {
    if (nums[right] !== 0) {
      // 交换非零元素与 left 位置的 0
      [nums[left], nums[right]] = [nums[right], nums[left]];
      left++; // left 前进,继续指向下一个 0
    }
  }
}

时间复杂度:O(n) —— 仅一次遍历
空间复杂度:O(1) —— 仅用两个指针变量
原地修改:直接操作 nums,无额外数组


🔍 为什么这个方法能保持顺序?

因为 right从左到右顺序扫描的,每当遇到非零元素,就把它放到当前最靠前的“空位”(即 left)。
由于非零元素是按出现顺序被放置的,相对顺序天然保持不变


🧪 扩展思考

  • 如果题目改成“把负数移到前面,正数移到后面,0 不动”?
    → 可用三指针分区思想(类似荷兰国旗问题)。
  • 如果允许新建数组?
    → 两趟扫描更简单,但不符合本题“原地”要求。

总结

“移动零”是一道经典的双指针入门题,它教会我们:

  • 如何用两个指针分工协作(一个找,一个占位);
  • 如何在不破坏顺序的前提下原地重排数组;
  • 交换比覆盖+补零更优雅(一次遍历,逻辑统一)。

掌握这种思维,你离 LeetCode Hot 100 又近了一步!

image.png

记住:不是所有双指针都从两端开始——有时,快慢指针同向而行,才是最优解。