LeetCode

LeetCode
Lichermione可以,这一题你的整体思路是对的,但有一句需要改得更严谨:
“慢指针右边全部都是零”并不始终成立。
更准确的说法是:在遍历过程中,[0, slow) 区间一定都是已经处理好的非零元素;[slow, fast) 是已经扫描过、等待被后续非零元素交换处理的区域;[fast, n) 是还没有扫描的区域。
你可以把整篇笔记优化成下面这样。
1. 移动零
LeetCode 283:移动零
题目描述
给定一个数组 nums,将所有 0 移动到数组末尾,同时保持所有非零元素的相对顺序。
要求:
- 必须原地修改数组;
- 不能复制数组;
- 非零元素之间的相对顺序不能改变。
例如:
1 | 输入: |
一、核心思路:双指针
使用两个指针:
1 | slow:慢指针 |
快指针 fast
负责遍历整个数组,寻找非零元素。
慢指针 slow
表示:
下一个非零元素应该放置的位置。
因此在遍历过程中始终满足:
1 | [0, slow) |
这一段全部都是已经处理好的非零元素,并且相对顺序保持不变。
可以理解成:
1 | 0 n |
二、具体处理过程
对于快指针 fast 指向的元素,有两种情况。
1. nums[fast] == 0
说明当前元素就是需要移动到后面的 0。
不需要处理:
1 | fast++; |
继续寻找下一个非零元素。
2. nums[fast] != 0
说明发现了一个非零元素。
应该把它放到慢指针 slow 所表示的位置:
1 | swap(nums[slow], nums[fast]); |
然后快指针继续向后遍历。
三、执行过程
例如:
1 | nums = [0, 1, 0, 3, 12] |
初始:
1 | slow = 0 |
第一次
1 | fast = 0 |
遇到 0,不处理:
1 | [0, 1, 0, 3, 12] |
第二次
1 | fast = 1 |
遇到非零元素,与 nums[slow] 交换:
1 | swap(nums[0], nums[1]) |
得到:
1 | [1, 0, 0, 3, 12] |
然后:
1 | slow++ |
此时:
1 | slow = 1 |
第三次
1 | fast = 2 |
遇到零,不处理。
第四次
1 | fast = 3 |
交换:
1 | swap(nums[1], nums[3]) |
得到:
1 | [1, 3, 0, 0, 12] |
然后:
1 | slow = 2 |
第五次
1 | fast = 4 |
交换:
1 | swap(nums[2], nums[4]) |
得到:
1 | [1, 3, 12, 0, 0] |
最终完成。
四、C 语言实现
1 | void moveZeroes(int* nums, int numsSize) |
这里:
1 | fast |
负责遍历整个数组。
而:
1 | slow |
始终指向:
1 | 下一个非零元素应该存放的位置 |
五、C++ 实现
C++ 可以直接使用 swap:
1 | class Solution |
代码非常简洁。
六、另一种写法:覆盖 + 补零
除了交换法,还可以分成两步。
第一步:
把所有非零元素依次放到数组前面。
1 | size_t index = 0; |
例如:
1 | 原数组: |
第二步:
把剩余位置全部补 0:
1 | for (size_t i = index; i < nums.size(); ++i) |
最终:
1 | [1, 3, 12, 0, 0] |
完整代码:
1 | class Solution |
七、两种方法比较
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
双指针 + swap |
O(n) |
O(1) |
一次遍历,代码简洁 |
| 覆盖 + 补零 | O(n) |
O(1) |
思路直观,但需要两次处理 |
这道题我更推荐:
1 | 双指针 + swap |
八、为什么不会破坏非零元素的相对顺序?
因为快指针:
1 | 从左向右 |
依次寻找非零元素。
找到一个,就依次放到:
1 | slow = 0 |
因此:
1 | 原数组: |
非零元素始终按照原来的顺序被放到数组前面。
所以这是一个稳定处理。
九、双指针真正需要记住的含义
这道题不要死记:
1 | 一个快指针 |
真正应该理解的是:
fast
1 | 负责寻找 / 遍历 |
slow
1 | 负责记录下一个有效元素应该存放的位置 |
也就是:
1 | fast:找数据 |
这是很多数组双指针题目的共同模型。
十、与快速排序的联系
这道题的思想和快速排序中的 partition(分区) 有一定相似性。
本质上都是:
1 | 遍历数组 + 根据条件划分区间 |
移动零:
1 | 非零元素 | 零元素 |
快速排序 partition:
1 | 小于 pivot | 大于等于 pivot |
例如:
1 | if (nums[i] < pivot) |
你会发现结构非常像:
1 | if (nums[i] != 0) |
所以这道题实际上也是一种简单的 partition 分区思想。
十一、总结
这道题核心只有一句话:
快指针寻找非零元素,慢指针记录下一个非零元素应该存放的位置。
执行规则:
fast从头到尾遍历数组。nums[fast] == 0:不处理,继续遍历。nums[fast] != 0:交换nums[fast]和nums[slow]。- 交换完成后
slow++。 - 当
fast遍历结束时,数组自然变成:
1 | 非零元素 | 零元素 |
时间复杂度:
1 | O(n) |
空间复杂度:
1 | O(1) |
面试时可以直接概括
1 | 使用快慢双指针。 |
这版比你原来的一个关键提升,就是把 slow 的含义定义清楚了:slow 不是简单的“慢指针”,而是“下一个有效元素应该放置的位置”。以后做删除元素、移除重复项、按条件划分数组等题时,都可以沿用这个理解。