欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
前言:
基于数据结构的扎实基础,算法思想能够有效提升问题解决的效率。为此,我们开设一个专门的算法专栏,用来探讨各类算法题目的解决方案。
在算法学习的道路上,双指针是一种简洁、高效且应用广泛的解题思想,它并非局限于某一种特定的数据结构,而是一种通过设置两个"标记点"(指针),协同遍历、筛选或修改数据,从而简化问题复杂度的核心思路。无论是数组、链表的遍历处理,还是数值组合、环形问题的求解,双指针都能发挥其独特优势------相较于暴力枚举的多层循环,它往往能将时间复杂度从O(n²)优化至O(n),大幅提升代码运行效率,同时让解题逻辑更清晰、代码更简洁易懂。
本文将系统拆解双指针算法的核心逻辑,按"快慢双指针""首尾双指针""快慢双指针解决环形问题"三大类别,结合LeetCode经典例题(如移动零、盛水最多的容器、三数之和、环形链表等),从算法思想、解题步骤、代码实现三个维度,手把手带你掌握双指针的用法。每道例题均搭配详细解析和优化思路,兼顾基础入门与进阶提升,帮助你真正理解双指针的核心逻辑。
文章目录
- 前言:
- 正文:
-
- 零、什么是双指针
- 一、快慢双指针
-
- [0. 算法思想:](#0. 算法思想:)
- [1. 双指针快排思想](#1. 双指针快排思想)
- [2. 双指针移动零](#2. 双指针移动零)
- [3. 复写零](#3. 复写零)
- 三、首尾双指针
-
- [0. 算法思想:](#0. 算法思想:)
- [1. 双指针移动零](#1. 双指针移动零)
- [2. 盛水最多的容器](#2. 盛水最多的容器)
- [3. 两数之和](#3. 两数之和)
- [4. 三数之和](#4. 三数之和)
- [5. 有效三角形的个数](#5. 有效三角形的个数)
- [6. 四数之和](#6. 四数之和)
- 三、快慢双指针解决环形问题
-
- [0. 算法思想:](#0. 算法思想:)
- [1. 环形链表](#1. 环形链表)
- [2. 快乐数](#2. 快乐数)
正文:
零、什么是双指针
需要说明的是,双指针并不局限于字面意义上的两个指针,而是通过标记两个目标点来简化问题的解决思路。接下来,我们通过具体实例深入解析这一算法。
一、快慢双指针
0. 算法思想:
前后快慢指针以慢指针为基准划分界限:慢指针左侧(包括其指向的元素)均小于等于基准值,右侧均大于基准值;或者是慢指针左或者右为同一元素。
1. 双指针快排思想

快速排序最初由霍尔(Hoare)提出,其算法逻辑相对复杂。为便于理解,后来发展出了前后指针法实现快排。以下是详细说明:
注:本文提到的"指针"并非真正意义上的指针,而是数组的索引下标
关键概念说明:
key [基准值]
prev [慢指针]
cur [快指针]
- 首先选择最左侧元素作为基准值 key ,初始化 prev 指针与 key 同位置,cur 指针指向下一个位置
- 当 cur 指向的元素小于 key 时,移动 prev 指针并与 cur 进行元素交换(可以避免原地互换)
- cur 指针持续移动,遇到比基准值小的元素时暂停,此时移动 prev 指针并进行交换(非原地交换会将较小元素移至 prev 位置)
- 重复上述过程直到 cur 指针越界
- 最后交换 key 与 prev 指向的元素,确保基准值左侧元素均小于它,右侧元素均大于它
cpp
//双指针法快排
int ptrSort(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
prev++;
if (a[cur] != a[prev])
{
Swap(&a[cur], &a[prev]);
}
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
至此完成对基准值的排序,之后只需递归地对左右两个子区间执行相同操作(一般使用递归),即可完成整个数组的排序。
2. 双指针移动零
- 题目:leetcode:移动零

移动零问题可以类比为以零为基准的单次快速排序:
- 初始化两个指针:cur指向数组首元素,prev初始化为-1
- 遍历过程中:
- 当cur遇到非零元素时,prev右移一位并与cur交换元素
- 否则cur继续右移
- 重复上述过程直至cur到达数组末尾
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
for(int cur = 0,dest = -1;cur<nums.size();cur++)
if(nums[cur])
swap(nums[++dest],nums[cur]);
}
};
3. 复写零
- 题目:leetcode:复写零

处理复写零问题的关键在于确定结束位置。以下是优化后的双指针解决方案:
- 初始化指针:将cur和dest都指向数组首元素,然后cur开始移动
- 遍历规则:
- 当cur指向0时:dest移动两步
- 当cur指向非零元素时:dest移动一步
- 终止条件:当dest到达数组末尾时停止
- 边界处理:
- 若cur指向最后一个元素为0时,dest可能越界
- 此时需要将dest回退两位,并将末尾元素置零,同时cur回退一位
- 逆向复写:由于已精确计算复写元素数量,无需担心原地复写导致的覆盖问题
cpp
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
//双指针找最后一个复写的数
int cur = 0,dest = -1,n = arr.size();
while(1)
{
if(arr[cur] == 0)
dest++;
dest++;
if(dest >= n-1)
break;//找到退出
cur++;
}
//处理特殊情况
if(dest == arr.size())
{
cur--;
dest -= 2;
arr[arr.size()-1] = 0;
}
//从后向前复写
while(cur >= 0)
{
if(arr[cur] == 0)
arr[dest--] = arr[cur];
arr[dest--] = arr[cur];
cur--;
}
}
};
三、首尾双指针
0. 算法思想:
首尾分别设定指针,可以交换元素解决移动零类似问题;也可以利用单调性解决其他问题
1. 双指针移动零
- 题目:leetcode:移动零

如果不需要保持元素的先后顺序的话,也可以使用首尾双指针法
- 设置左右指针,left 和 right
- left 找0,right 找非零元素,交换
- 直到左右指针相遇
cpp
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0,right = nums.size()-1;
while(left < right)
{
if(nums[left]) left++;
if(nums[right] == 0) right--;
if(left < right && nums[left] == 0 && nums[right] != 0) swap(nums[left],nums[right]);
}
}
};

2. 盛水最多的容器
- 题目:leetcode:盛水最多的容器

- 解法一:
本题可以使用暴力解法来计算,即两层循环计算出 "体积" , 取其最大值。 - 解法二:
根据体积的计算公式 v = w * h :
- 先排序,得到一个有顺序的数组
- 首尾设定指针,向内移动,宽度w必定减小,高度由于取最小值,会降低或者不变
- 则说明:计算出体积v以后,将二者高度较小的一方排除,向内移动,即可减少遍历次数
cpp
class Solution {
public:
int getv(vector<int>& height, int left,int right)
{
int width = right - left;
int high = height[right];//假设法
if(height[left] < height[right])
{
high = height[left];
}
return width * high;
}
int maxArea(vector<int>& height)
{
int left = 0;
int right = height.size() - 1;
int v = 0;
while(left < right)
{
int vv = getv(height, left, right);
if(height[left] < height[right]) left++;
else right--;
if(vv > v) v = vv;
}
return v;
}
};
3. 两数之和
-
题目:两数之和

-
解法一:
双层循环,暴力枚举出所有可能值,找到两数之和为目标的两个数。
-
解法二:
- 先排序,依旧设定首位指针
- 于是存在:
- 如果两指针所指元素之和小于目标值:变小一定得不到目标值,所以右指针不能左移,左指针右移
- 如果两指针所指元素之和大于目标值:变大一定得不到目标值,所以左指针不能右移,右指针左移
- 遍历直到找到和为目标值的两个数
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& price, int target)
{
int left = 0, right = price.size() - 1;
while(left < right)
{
if(price[left] + price[right] > target)
right--;
else if(price[left] + price[right] < target)
left++;
else
return vector<int>({price[left],price[right]});
}
return vector<int>();
}
};
4. 三数之和
- 题目:三数之和

三数之和实际上是两数之和的变式:
解法一:
- 与两数之和相同,三层循环可以枚举出想要结果,找到以后加入数组即可
解法二:
- 依旧可以先排序,然后固定首尾其中一个数字,依次顺序固定,重复元素跳过
- 用两数之和类似双指针法,计算出三数之和为结果的数字,需要注意的是,重复元素跳过
- 最后遍历完成,结果加入二维数组
cpp
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
//优化
sort(nums.begin(),nums.end());
int n = nums.size();
//利用单调性,定尾部,前面走双指针
vector<vector<int>> vv;
for(int i = n-1;i >= 2;i--)
{
if ( i < n-1 && nums[i] == nums[i + 1])
{
continue;
}
int left = 0,right = i-1;
while(left < right)
{
if(nums[left] + nums[right] + nums[i] > 0) right--;
else if(nums[left] + nums[right] + nums[i] < 0) left ++;
else
{
// 找到符合条件的三元组,加入结果
vv.push_back({nums[left], nums[right], nums[i]});
// 跳过左指针重复值
while (left < right && nums[left] == nums[left + 1]) ++left;
// 跳过右指针重复值
while (left < right && nums[right] == nums[right - 1]) --right;
// 双指针收缩,继续找下一组
++left;
--right;
}
}
}
return vv;
}
};
5. 有效三角形的个数
- 题目:有效三角形的个数

实际上本题也算是三数之和的变式,只不过本体要求三数组成三角形而不是和为零:
解法一:三层循环遍历枚举
解法二:
- 先排序,依旧固定一边
- 利用三角形两小边之和大于第三边性质寻找三个符合要求的边,符合要求则计入次数
cpp
class Solution {
public:
int triangleNumber(vector<int>& nums) {
int n = nums.size()-1;
sort(nums.begin(),nums.end());
int sum = 0;
for(int i = n; i>0; i--)
{
int left = 0,right = i-1;
while(left < right)
{
if(nums[left] + nums[right] > nums[i])
{
sum += right-left;
right--;
}
else
left++;
}
}
return sum;
}
};
6. 四数之和
- 题目:四数之和

这里不再讲解原理,原理与三数之和类似,直接看代码: - 提一点:本题需要注意的是样例里面存在超大数,所以需要注意 int 无法承载的问题
cpp
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
int n = nums.size();
sort(nums.begin(),nums.end());
vector<vector<int>> vv;
for(int j = n - 1; j >= 3;j--)
{
if(j < n-1 && nums[j] == nums[j+1]) continue;
int t = target - nums[j];
for(int i = j - 1; i >= 2;i--)
{
if(i < j-1 && nums[i] == nums[i+1]) continue;
int left = 0,right = i - 1;
while(left < right)
{
if((long long)nums[left] + nums[right] + nums[i] < (long long)t) left++;
else if ((long long)nums[left] + nums[right] + nums[i] > (long long)t) right--;
else
{
vv.push_back({nums[left], nums[right], nums[i],nums[j]});
while(left < right && nums[left] == nums[left + 1]) left++;
while(left < right && nums[right] == nums[right - 1]) right--;
left++,right--;
}
}
}
}
return vv;
}
};
三、快慢双指针解决环形问题
0. 算法思想:
用跑圈的问题来类比,如果一个快的人和慢的人在环形跑道跑步,如果他们一直是恒速前进,那么必定相遇,快慢双指针也类似这个问题
1. 环形链表
程序员遇到的经典环形问题是:leetcode:环形链表问题
采用快慢指针的思想,如果有环就必定能相遇,而如果没环必定能越界
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
typedef struct ListNode ListNode;
ListNode* fast = head;
ListNode* slow = head;
while(fast!=NULL && fast->next!= NULL)
{
fast = fast->next->next;
slow = slow->next;
if(slow == fast)
{
return true;
}
}
return false;
}
};
2. 快乐数
- 题目:leetcode:快乐数

本题的两种情况其实可以看作是一种情况:
- 有环:环内无1,成一个大环
- 有环:环内只有1,可看作一循环,即为快乐数
所以只需要判断快慢指针相遇的时候他们所指元素的值是不是1,即可判断是否是快乐数
cpp
class Solution {
public:
int get_sosq(int& n)//获取平方和
{
int sum = 0;
while (n)
{
sum += pow((n % 10), 2);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
int show = n, fast = n;
show = get_sosq(show);
fast = get_sosq(fast);
fast = get_sosq(fast);
while (show != fast)
{
show = get_sosq(show);
fast = get_sosq(fast);
fast = get_sosq(fast);
}
if (fast == 1)
return true;
return false;
}
};
- 本节完...