【双指针专题】之移动零
一、题目描述
给定一个整数数组
nums
,将所有的 0 移动到数组末尾,同时保持非零元素的相对顺序。要求:
- 必须 原地操作,不可开辟额外数组。
- 要尽量减少不必要的写操作。
示例:
- 输入:
[0,1,0,3,12]
- 输出:
[1,3,12,0,0]
- 输入:
[0]
- 输出:
[0]
这是经典的 双指针问题。
二、思路引导:双指针 / 划分思想
把数组想象成两个区域:
- 左侧:已经处理好的 非零区间
- 右侧:未处理的区间
我们用两个指针:
cur
:扫描数组;dest
:非零区间的末尾索引。循环不变式:
在任意时刻,[0..dest]
全是非零元素,且顺序保持;
区间(dest, cur)
全是零。每当遇到非零,就把它扩展到非零区间尾部。最终整个数组左边是非零,右边自然就是零。
三、标准解法:交换法
下面是直观且常用的写法:
#include <vector>
#include <algorithm>
using namespace std;class Solution {
public:void moveZeroes(vector<int>& nums) {int dest = -1; // 非零区间末尾,初始为 -1for (int cur = 0; cur < (int)nums.size(); ++cur) {if (nums[cur] != 0) {// 遇到非零,就把它放到非零区间末尾++dest;swap(nums[dest], nums[cur]);}}}
};
运行示例:
- 输入:
[0,1,0,3,12]
关键步骤:
- cur=1: 交换
nums[0]
和nums[1]
→[1,0,0,3,12]
- cur=3: 交换
nums[1]
和nums[3]
→[1,3,0,0,12]
- cur=4: 交换
nums[2]
和nums[4]
→[1,3,12,0,0]
四、代码剖析与细节问题
1. 为什么 dest 初始为 -1 ?
因为刚开始没有非零区间,赋值 0 不合适,因而赋值 -1 ,这样
++dest
后第一次正好是 0
2. swap 的意义?
++dest
:先把非零区间末尾向右扩一位;swap(nums[dest], nums[cur])
:把当前非零值“放”进去;- 当
cur == dest
时,就是自交换。
3. 相对顺序是否保持?
保持,因为我们按顺序依次把非零写到前面。
4. 边界情况:
- 空数组 → 直接返回;
- 全 0 → 结果全 0;
- 全非零 → 数组保持不变。
五、复杂度分析与正确性说明
- 时间复杂度:
单次遍历数组 →
O(n)
。
- 空间复杂度:
只用常数变量 →
O(1)
。
- 正确性保证(不变式):
恒为非零区间,顺序正确; [0..dest]
遍历结束时,剩余部分自然全是零。
六、常见坑总结
- 为什么不用双指针对撞?
因为零可能分散在数组中,对撞会打乱相对顺序。
- 自交换会不会影响效率?
不会,代价极小,也不影响正确性。
- 能不能用 STL 一行解决?
可以用
stable_partition
,但笔者更建议手写双指针法。
- 如果要移动指定值,而不是 0 ?
把条件
nums[cur] != 0
改成nums[cur] != val
即可。
到这里,本文就结束啦。如果对读者有帮助,欢迎点赞、收藏和评论!
后续笔者会继续更新「双指针专题」系列,带你逐一攻克经典题目。