题目:给定一个数组
nums,编写一个函数将所有0移动到数组末尾,同时保持非零元素的相对顺序。要求:原地操作,不复制数组。
这道题看似简单,但要在满足“原地”和“保序”的前提下高效完成,却藏着对指针思维的巧妙考验。今天,我们就用 真正的双指针交换法,一次遍历、O(1) 空间,优雅破局!
❌ 常见误区:为什么不能直接删 0 再补?
很多初学者会想到:
// 错误示范!
for (let i = 0; i < nums.length; i++) {
if (nums[i] === 0) {
nums.splice(i, 1); // 删除 0
nums.push(0); // 末尾补 0
}
}
但这样做有两个致命问题:
splice是 O(n) 操作,整体复杂度退化为 O(n²);- 修改数组长度会导致索引错乱,可能漏掉连续的 0。
更重要的是——题目明确要求“原地操作”,隐含期望是 O(n) 时间 + O(1) 空间。
正解:双指针交换法(一次遍历)
🧠 核心思想
我们维护两个指针:
left:始终指向当前第一个 0 的位置(即下一个非零元素应该放的位置);right:从左到右扫描整个数组,寻找非零元素。
当 right 遇到非零数时,立即与 left 位置交换,然后 left++。
这样,所有非零数按顺序被“搬”到前面,0 自然被“挤”到后面。
💡 关键洞察:
left左侧全是非零数,left到right之间全是 0。
🖼️ 图解过程
以 nums = [0, 1, 0, 3, 12] 为例:
| 步骤 | right | nums[right] | 操作 | 数组状态 | left |
|---|---|---|---|---|---|
| 初始 | - | - | - | [0,1,0,3,12] | 0 |
| 1 | 0 | 0 | 跳过 | [0,1,0,3,12] | 0 |
| 2 | 1 | 1 ≠ 0 | 交换 0↔1 | [1,0,0,3,12] | 1 |
| 3 | 2 | 0 | 跳过 | [1,0,0,3,12] | 1 |
| 4 | 3 | 3 ≠ 0 | 交换 0↔3 | [1,3,0,0,12] | 2 |
| 5 | 4 | 12 ≠ 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 又近了一步!
记住:不是所有双指针都从两端开始——有时,快慢指针同向而行,才是最优解。