Appearance
实战:移动零
这道题是双指针的另一个经典应用。与上一题"删除重复项"不同的是,这里我们需要交换元素,而不是简单地覆盖。
题目描述
LeetCode 283. 移动零
给定一个数组
nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。请注意,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入:nums = [0, 1, 0, 3, 12]
输出:[1, 3, 12, 0, 0]示例 2:
输入:nums = [0]
输出:[0]题目分析
关键点:
- 原地修改:不能创建新数组
- 保持相对顺序:非零元素的先后顺序不能变
- 零移到末尾:所有 0 都要在数组最后
思考一下:如果不要求保持相对顺序,怎么做?
可以用首尾双指针,左指针找 0,右指针找非零,交换它们。但这会打乱非零元素的顺序。
要保持相对顺序,就要用快慢指针,从同一方向遍历。
解法一:双指针 + 交换
核心思路:
slow指针:指向下一个非零元素应该放置的位置fast指针:遍历数组,寻找非零元素- 遇到非零元素,就和
slow位置的元素交换
javascript
function moveZeroes(nums) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== 0) {
// 交换 slow 和 fast 位置的元素
[nums[slow], nums[fast]] = [nums[fast], nums[slow]];
slow++;
}
}
}执行过程
以 nums = [0, 1, 0, 3, 12] 为例:
初始:slow = 0, fast = 0
[0, 1, 0, 3, 12]
↑
s,f
fast=0: nums[0]=0, 是零 → 跳过
fast=1: nums[1]=1, 非零 → 交换 nums[0] ↔ nums[1]
[1, 0, 0, 3, 12], slow = 1
↑ ↑
s f
fast=2: nums[2]=0, 是零 → 跳过
fast=3: nums[3]=3, 非零 → 交换 nums[1] ↔ nums[3]
[1, 3, 0, 0, 12], slow = 2
↑ ↑
s f
fast=4: nums[4]=12, 非零 → 交换 nums[2] ↔ nums[4]
[1, 3, 12, 0, 0], slow = 3
↑ ↑
s f
结果:[1, 3, 12, 0, 0] ✓为什么能保持相对顺序?
因为 fast 是从左到右遍历的,非零元素按原来的顺序被移到前面。
复杂度分析:
- 时间复杂度:O(n)——遍历一次数组
- 空间复杂度:O(1)——只用了两个指针变量
优化:避免自己和自己交换
当 slow === fast 时,交换自己和自己没有意义。可以加个判断:
javascript
function moveZeroes(nums) {
let slow = 0;
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== 0) {
if (slow !== fast) {
[nums[slow], nums[fast]] = [nums[fast], nums[slow]];
}
slow++;
}
}
}这个优化在数组开头没有零的情况下可以减少一些操作,但整体复杂度不变。
解法二:覆盖 + 补零
另一种思路:先把所有非零元素移到前面,然后把剩余位置填零。
javascript
function moveZeroes(nums) {
let slow = 0;
// 第一遍:把非零元素移到前面
for (let fast = 0; fast < nums.length; fast++) {
if (nums[fast] !== 0) {
nums[slow] = nums[fast];
slow++;
}
}
// 第二遍:剩余位置填零
while (slow < nums.length) {
nums[slow] = 0;
slow++;
}
}执行过程:
[0, 1, 0, 3, 12]
第一遍:移动非零元素
fast=0: 0, 跳过
fast=1: 1, nums[0]=1, slow=1
fast=2: 0, 跳过
fast=3: 3, nums[1]=3, slow=2
fast=4: 12, nums[2]=12, slow=3
数组变成:[1, 3, 12, 3, 12]
↑
slow=3
第二遍:填零
nums[3]=0, nums[4]=0
结果:[1, 3, 12, 0, 0] ✓复杂度分析:
- 时间复杂度:O(n)——两次遍历,总共 O(2n) = O(n)
- 空间复杂度:O(1)
两种解法对比
| 方法 | 遍历次数 | 操作类型 | 特点 |
|---|---|---|---|
| 交换法 | 1 次 | 交换 | 代码简洁 |
| 覆盖法 | 2 次 | 覆盖 + 填充 | 逻辑清晰 |
两种方法的时间复杂度都是 O(n),空间复杂度都是 O(1)。
在实际中,交换法可能稍快一点,因为只遍历一次。但覆盖法的逻辑更直观。
选择哪种取决于个人偏好。
常见错误
- 没有保持相对顺序:使用首尾双指针会打乱顺序
- 直接删除零:用
splice删除元素会导致 O(n²) 复杂度
相关题目
- 27. 移除元素:移除所有等于 val 的元素(与本题思路相同)
- 26. 删除有序数组中的重复项:上一章学过
本章小结
这一章我们又练习了一次快慢双指针:
- 快指针:遍历数组,找非零元素
- 慢指针:记录下一个非零元素应该放的位置
- 两种实现:交换法(一次遍历)和覆盖法(两次遍历)
双指针的核心思想是:用一个指针读取,用另一个指针写入。
这个模式会反复出现,一定要熟练掌握。
下一章,我们来看一道需要更多技巧的题目:「合并两个有序数组」。