在算法中,双指针(Two Pointers) 是一种极具代表性的高效技巧。它通过维护两个索引或指针,在一次遍历中完成双层循环才能解决的问题,从而显著降低时间复杂度。常见的双指针形态主要分为两类:对撞指针和快慢指针。这两种形式几乎覆盖了所有数组、字符串与链表中的高频算法场景,是算法思维中必须掌握的核心技巧。



right指针(快指针)用来遍历整个数组,另外一个left指针(慢指针)用来记录下一个非零元素应该存放的位置。right指针扫描到非零元素时,就先将left++,因为开始指向的位置在-1处,然后将其移动到left指针所指的位置。这样可以确保所有非零元素按照原有顺序被前移。right指针继续向后遍历,直到扫描完整个数组。right指针遍历结束后,left指针之前的部分已经全部是非零元素,left指针及其之后的位置则应当全部填充为 0。class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = -1;
int right = 0;
for (right = 0; right < nums.size(); right++)
{
if (nums[right] != 0)
swap(nums[right], nums[++left]);
}
}
};//零移动和复写零
#include<iostream>
#include<vector>
using namespace std;
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = -1;
int right = 0;
for (right = 0; right < nums.size(); right++)
{
if (nums[right] != 0)
swap(nums[right], nums[++left]);
}
}
};
int main()
{
vector<int> nums = { 0,1,0,3,2 };
Solution().moveZeroes(nums);
for (auto e : nums)
{
cout << e << " ";
}
cout << endl;
return 0;
}


当我们正着复写下面这个数组的时候:
arr = [1, 0, 0, 2, 3]我们在处理第一个 0 时,就提前右移覆盖掉了第二个 0; 第二个 0 还没来得及被复写,它就被前一步操作的结果顶掉了;最终结果变成了错误的:
[1, 0, 0, 0, 2]实际正确结果应是:
[1, 0, 0, 0, 0]fast 表示)会超过数组的实际长度,而慢指针 slow 则用来记录当前正在处理的元素位置。遍历过程中,fast 会根据元素是否为 0 来累加长度:若元素不是 0,长度加 1;若是 0,则长度加 2。这样,当 fast 的值刚好达到或超过数组长度时,就说明慢指针的位置到达了数组中最后一个需要处理的元素。slow 和 fast 两个指针的末端开始,从右向左遍历。这样做的好处是可以避免在复写 0 时覆盖掉尚未处理的元素。如果当前元素(slow指向的元素)是非零,则直接将其写入当前位置(fast指向的位置);若是 0,则需要写入两次(一个当前位置、一个前一位置),并且注意边界防止越界写入。通过这种倒序的处理方式,我们能在不额外开辟空间的情况下完成所有复写操作。如图:我们通过这段示例可以知道找到在复写中最后一个需要处理的元素是4

int slow = 0;
int fast = -1;
int n = arr.size();
//找最后一个数
while (slow < n)
{
if (arr[slow]) fast++;
else fast += 2;
if (fast >= n - 1) break;
else slow++;
}接下来我们进行第二步,从右向左遍历进行复写

fast == n 时,说明最后一次复写操作正好溢出了数组长度。例如数组 [1,0,2,3,0,4],长度为 5,最后一个元素是 0。复写这个 0 需要写入两个 0(一个是原来的,一个是复写的),但第二个 0 会落在下标 5 上,也就是数组边界之外。这时我们只能在数组的最后一个合法位置(下标 n−1)写入一个 0,然后将其余逻辑稍作调整。 //处理边界情况
if (fast == n)
{
arr[n - 1] = 0;
slow--;
fast -= 2;
}arr[n - 1] = 0;,在最后一个位置写入一个 0;接着 slow-- 将原数组指针向前移动一位,表示刚才的 0 已经部分处理完成,不再继续参与复写;最后 fast -= 2 是为了让复写指针同步回退两位,确保后续从后往前的填充操作可以正确对应。//从后向前完成复写操作
while (slow >= 0)
{
if (arr[slow]) arr[fast--] = arr[slow--];
else
{
arr[fast--] = 0;
arr[fast--] = 0;
slow--;
}
}class Solution2{
public:
void duplicateZeros(vector<int>& arr)
{
int slow = 0;
int fast = -1;
int n = arr.size();
//找最后一个数
while (slow < n)
{
if (arr[slow]) fast++;
else fast += 2;
if (fast >= n - 1) break;
else slow++;
}
//处理边界情况
if (fast == n)
{
arr[n - 1] = 0;
slow--;
fast -= 2;
}
//从后向前完成复写操作
while (slow >= 0)
{
if (arr[slow]) arr[fast--] = arr[slow--];
else
{
arr[fast--] = 0;
arr[fast--] = 0;
slow--;
}
}
}
};//零移动和复写零
#include<iostream>
#include<vector>
using namespace std;
class Solution2{
public:
void duplicateZeros(vector<int>& arr)
{
int slow = 0;
int fast = -1;
int n = arr.size();
//找最后一个数
while (slow < n)
{
if (arr[slow]) fast++;
else fast += 2;
if (fast >= n - 1) break;
else slow++;
}
//处理边界情况
if (fast == n)
{
arr[n - 1] = 0;
slow--;
fast -= 2;
}
//从后向前完成复写操作
while (slow >= 0)
{
if (arr[slow]) arr[fast--] = arr[slow--];
else
{
arr[fast--] = 0;
arr[fast--] = 0;
slow--;
}
}
}
};
int main()
{
vector<int> nums1 = { 0,1,0,3,2 };
Solution2().duplicateZeros(nums1);
for (auto e1 : nums1)
{
cout << e1 << " ";
}
cout << endl;
return 0;
}
在本篇文章中,我们通过「移动零」与「复写零」两个经典题目,深入理解了 双指针(Two Pointers) 的核心思想与应用方式。🚀 下一篇预告:快乐数 & 盛水最多的容器
下一篇,我们将继续探索双指针与数学思维的结合应用:
敬请期待下一篇: 【C++】优选算法必修篇之双指针实战:快乐数 & 盛水最多的容器