目录
概念:
双指针算法是一种常用的编程技巧,核心思想是使用两个指针(或索引)在数据结构(通常是数组或链表)中移动,通过合理调整指针位置来高效解决问题。它的主要优势是能将原本需要嵌套循环(时间复杂度 O (n²))的问题优化为线性时间复杂度(O (n)),从而大幅提升效率。
双指针的常见应用场景和实现方式:
-
相向指针:两个指针从数组两端向中间移动
- 典型应用:二分查找、两数之和(有序数组)、判断回文串等
- 工作方式:根据条件判断移动左指针(增大)或右指针(减小)
-
同向指针:两个指针从同一端出发,向相同方向移动
- 典型应用:滑动窗口问题、移除重复元素、寻找最长子串等
- 工作方式:快指针先行探索,慢指针负责记录有效位置
-
分离指针:两个指针分别处理不同的序列
- 典型应用:合并两个有序数组、判断链表是否相交等
- 工作方式:分别遍历两个序列,根据条件移动相应指针
双指针算法的关键在于找到指针移动的规则,确保每个元素最多被访问一次。这种技巧特别适合处理有序数据结构,能在不使用额外空间(或仅使用常数空间)的情况下解决问题,兼顾了时间和空间效率。
例如,在有序数组中寻找两数之和等于目标值的问题:
- 左指针初始在数组开头,右指针在数组末尾
- 如果两数之和等于目标值,找到答案
- 如果和小于目标值,左指针右移(增大总和)
- 如果和大于目标值,右指针左移(减小总和)
- 直到找到答案或指针相遇
这种方法只需一次遍历即可完成,时间复杂度为 O (n),远优于暴力解法的 O (n²)。
实战例题(深度刨析每一道经典例题)
leetcode283例题

算法原理讲解:
注意遇到这种题目,读完之后发现他是给一个区域,也就是数组进行分块
所以这类题可以叫数组分块或者数组划分,一边是非0数字,一边是0数字
非0数字还要保证其相对顺序没有发生改变
可以使用双指针:cur从前往后扫描,des标识要交换的第一个0的位置
也就是cur去找非0元素,des去标记0元素(两个指针都是从前往后)
cur从前往后找的时候:1.遇到0元素,cur++ 2.遇到!0元素和des位置(des始终会指向一个0元素)的元素交换
知道cur遍历完整个数组就算结束了
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int size=nums.size();
int cur=0;
int des=-1;
while(cur<size){
if(nums[cur]){
des++;
int tmp;
tmp=nums[des];
nums[des]=nums[cur];
nums[cur]=tmp;
}
cur++;
}
}
};
leetcode1089例题

算法原理讲解:
如果能够开辟额外的数组这道题是一道简单题,难点就在只能在原来数组上操作
可以先进行”异地操作“:也就是如果在开辟一个数组出来用两个指针能否完成
在进行”本地“迁移操作:是否能够在原先数组上通过双指针完成
发现:两个指针两个数组很简单,但是对于一个数组会从左往后一起走会出现问题
问题:des指针可能会走到cur指针的前面,也就是des指针会走的快,然后覆盖掉要检查的元素
解决方案:可以从右向左尝试,发现如果对于示例,只要知道最后一个元素应该填4,des指向最后一个位置,然后cur指向4,从右往左一起走,des指针就不会超过cur指针

这里的特殊情况就是如果倒数第二个数字是0的话,des可能就会超出原本数组的边界
所以要处理一下特殊情况
本道例题关键在于:des不能走到cur的前面,覆盖掉要检查的元素,之前283题不会覆盖一直都是cur走在最前面检查元素
class Solution {
public:
void duplicateZeros(vector<int>& arr) {
int n = arr.size();
int cur = 0;
int des = -1;
// 1. 找到最后一个需要被处理的元素
while (cur < n) {
if (arr[cur] == 0) {
des += 2;
} else {
des += 1;
}
// 当des超出范围时停止
if (des >= n - 1) {
break;
}
cur++;
}
// 2. 处理特殊情况:最后一个元素是0且刚好好超出数组
if (des == n) {
arr[n - 1] = 0;
des -= 2;
cur--;
}
// 3. 从后向前复制元素
while (cur >= 0 && des >= 0) {
if (arr[cur] == 0) {
arr[des] = 0;
des--;
arr[des] = 0;
} else {
arr[des] = arr[cur];
}
des--;
cur--;
}
}
};
leetcode202例题

算法原理讲解:
可以先自行举一下19这个快乐数,和2这个非快乐数
发现19这个快乐数最后变成1,然后一直以1循环
然后2这个快乐数会一直计算下去,最后会计算会4

对此我们可以抽象出来

如果是快乐数,那环内就会一直是1,如果非快乐数,那环内就不会是1
对此我们可以联想到数据结构中的链表:在判断一个链表是否成环的时候,我们当时使用了快慢指针的思路:快指针走两步,慢指针走一步,如果慢指针和快指针相遇了,说明有环
这里我们只需要结合快慢指针,并且判断环内相遇的时候的元素是否为1就行
这里是必定成环的,所以只要判断环内元素是几就行

注意:我们这节讲的双指针法不要局限于平时所学的*之类的,指针指针就是能够通过里面的地址找到元素就行,这里我们可以把每个元素本身定义为指针,例如fast一开始指向19,走两步fast值就变成68,slow就是82,所以我们每次更新fast和slow这个int类型的值就行,直到他们相同的时候判断一下相同的时候是1还是!1
这里扩展一下为什么一定成环
这里题目是有要求的有一个最大数,就是int=2的31次方-1,这里我们假设最大数为999
那9^2+9^2+9^2=243,也就是你的范围是【1,243】,你只要在999这个范围之内变换一定是会回到1-243的,假设你经过244次变换,前面243次每次都是1-243范围内某个值,但是在经过一次变换就会导致某个值中两次,所以就会成环

class Solution {
public:
bool isHappy(int n) {
int slow = n;
int fast = n;
int fastsum = 0;
int slowsum = 0;
// 快指针走两步,慢指针走一步,走完之后判断
while (1) {
while (fast) {
int first = fast % 10;
fastsum += first * first;
fast = fast / 10;
}
fast = fastsum;
fastsum = 0;
while (fast) {
int first = fast % 10;
fastsum += first * first;
fast = fast / 10;
}
fast = fastsum;
fastsum = 0;
while (slow) {
int first = slow % 10;
slowsum += first * first;
slow = slow / 10;
}
slow = slowsum;
slowsum = 0;
if (slow == fast) {
if (slow == 1) {
return true;
} else
return false;
}
}
}
};
leetcode11例题

算法原理讲解:
方法一:暴力求解
固定左边指针,右指针依次去遍历,然后记录下以左边固定右边移动的容积最大值
左边++,然后再依次移动右边,又记录最大值
所有的最大值再求一个最大值就可以得出来了
两个for循环就可以搞定,但是这个暴力求解会超时,时间复杂度为N^2
方法二:利用单调性求解
我们先看一个小区间比如6 2 5 4
如果是6和4分别做高,求出一个v1,这个v1=高*宽,宽=3-0=3,高=4,所以v1=12
但是如果是4和5呢?发现宽度减小,并且高度不变,明显这种选法低于v1
如果是4和2呢?宽度减小,高度减小,变成2,明显低于v1
所以下一步我们可以考虑6 2 5这个区间,直接排除掉4作为高这个区间,依次算出每个区间的v
回到题目,我们可以1和7先算一个v1,然后哪边矮删掉哪边,也就是左指针++
8和7做高算v2,右边矮所以右节点--
依次算出每个v,然后更新vmax就行了

class Solution {
public:
int maxArea(vector<int>& height) {
int vmax=0;
int left=0;
int right=height.size()-1;
while(left<right){
//1.先计算v
//1.1计算宽度
int weigh=right-left;
//1.2选择高度,矮的作为高度
int high=0;
if(height[left]>height[right])
high=height[right];
else
high=height[left];
vmax=max(vmax,high*weigh);
//2.移动指针,哪边小移动哪边
if(height[left]>height[right])
right--;
else
left++;
}
return vmax;
}
};
leetcode611例题

算法原理:根据题意,判断三个数是否能够构成三角形,我们只需要使最小的两个数a+b>c判断
由于示例说明给的数组不是一个排好序的数组,所以我们可以给数组排序,能够大大优化
第一种解法:暴力枚举
//伪代码
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
for(int k=j+1;k<n;k++)
check(i,j,k);
三层for循环枚举所有的三个数,如果数组没有经过排序,时间复杂度就是O(3*N^3),因为check里面需要比较三次,如果排好序的只要比较i+j>k即可
如果经过排序,时间复杂度就是O(NlogN+N^3),很明显时间优化对比上排序的更优化
第二种解法:利用单调性双指针法
假设数组(2,2,3,4,5,9,10)
先用一个指针p固定10,然后left指针指向2,right指针指向9
判断left+right>p?
有两种情况:
1.left+right>p,此时说明区间最小的和最大的相加>p,那你所有的left++,固定right都是>p的
所以此时我们只要算出个数也就是下标相减,5-0=5,说明这个区间有5个三角形
2.left+right<=p,此时说明区间最小的和最大的相加小于等于p,构不成三角形,那你所有的固定left,right--都是<=p的
所以针对两种情况,我们只需要第一种操作完right--,第二种操作完left++,不断地缩小区间,这样我们就能够固定p指针,算出以p为其中最大数的三角形
时间复杂度:固定p指针,遍历就是n次,然后left和right分别从两边走到中间,也就是遍历数组一遍,所以n里面套个n,那就是O(N^2)

class Solution {
public:
int triangleNumber(vector<int>& nums) {
sort(nums.begin(), nums.end());
int sum = 0;
int p = nums.size() - 1;
for (; p >= 2; p--) {
int left = 0;
int right = p - 1;
while (left < right) {
// left+right>p
if (nums[left] + nums[right] > nums[p]) {
sum += right - left;
right--;
} else {
left++;
}
}
}
return sum;
}
};
leetcode15例题

算法原理讲解:
第一种解法:暴力枚举,排序+暴力+去重(可以使用c++中的unordered set)
时间复杂度为O(N^3)
第二种解法:双指针
排序+双指针+去重

假设数组为如图上述
先用指针i固定左边,-4,然后再剩下的区间当中找和为4的两个数
比如固定-4,left指向-4,right指向6,-4+6=2<4,说明left要++,-1+6=5>4,right--
-1+5=4,这一组符合,所以-4,-1,5就是一组
当寻找完一组之后不能停,left++,right--同时移动缩小区间
这就能够在区间当中找到所有符合的三数之和等于0
注意去重可以使用容器,但是面试当中如果问还有别的解法我们也要会
去重:
找到一种结果之后,left++,right--的过程当中要跳过和之前一样的元素
当使用完指针i之后也要跳过重复的元素,否则比如你使用了-4,下一个还是-4就要跳过
还有优化的空间,比如i指向一个正数,就没必要在后面找了,正数+正数+正数是不可能等于0的
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> v;
sort(nums.begin(), nums.end());
int i = 0;
int reti = 100;
for (; i < nums.size(); i++) {
if (nums[i] > 0)
break;
int a = nums[i];
int left = i + 1;
int right = nums.size() - 1;
while (left < right) {
if (nums[left] + nums[right] + a > 0) {
right--;
} else if (nums[left] + nums[right] + a < 0) {
left++;
} else {
v.push_back({nums[i], nums[left], nums[right]});
while (left < right && nums[left] == nums[left + 1]) {
left++; // 持续移动left直到找到不同元素
}
while (left < right && nums[right] == nums[right - 1]) {
right--; // 持续移动right直到找到不同元素
}
left++;
right--;
}
}
// 对i进行去重
while (i + 1 < nums.size() && nums[i + 1] == nums[i]) {
i++;
}
}
return v;
}
};
leetcode18例题

算法原理讲解:
解法一:排序+暴力枚举+去重
解法二:沿用三数之和的思想:排序+双指针+去重

class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
vector<vector<int>> ret;
// 增加边界判断,避免数组长度不足4的情况
if (nums.size() < 4) {
return ret;
}
for (int a = 0; a < nums.size(); a++) {
for (int b = a + 1; b < nums.size(); b++) {
// 在left和right区间
long long sum = (long long)target - nums[a] - nums[b]; // 剩下两个数相加=sum
int left = b + 1;
int right = nums.size() - 1;
while (left < right) {
if (nums[left] + nums[right] < sum) {
left++;
} else if (nums[left] + nums[right] > sum) {
right--;
} else {
// 相等
ret.push_back(
{nums[a], nums[b], nums[left], nums[right]});
// 去重left和right
while (left < right && nums[left + 1] == nums[left]) {
left++;
}
while (left < right && nums[right - 1] == nums[right]) {
right--;
}
left++;
right--;
}
}
// 去重b
while (b < nums.size()-2 && nums[b + 1] == nums[b]) {
b++;
}
}
// 去重a
while (a < nums.size()-3 && nums[a + 1] == nums[a]) {
a++;
}
}
return ret;
}
};
双指针的理解
当题目符合 “需要遍历数组 / 链表,且暴力解法是 O (n²) 或更高,同时元素有排序 / 单调性 / 特定位置关系” 时,就可以优先考虑双指针。
遇到题目时,按这 3 步走,就能快速决策:
-
第一步:先想暴力解法,看时间复杂度比如 “三数之和”,暴力是三重循环(O (n³)),显然效率低;这时候就想 “能不能用更少的循环?”—— 双指针能把三重循环降为 O (n²)(排序 O (nlogn),遍历 i + 左右指针 O (n²)),符合优化需求。
-
第二步:看题目是否有 “单调性 / 排序 / 位置差” 的条件
- 如果数组是无序的,但暴力解法复杂度高,试试 “先排序” 再用双指针(如三数之和、四数之和);
- 如果是链表,且要找中点 / 环 / 倒数 k 个元素,直接用快慢指针(不用额外空间,比用栈 / 哈希表更优);
- 如果是子数组问题,且元素非负(保证窗口扩大时和递增,缩小时有递减),用滑动窗口(双指针维护窗口)。
-
第三步:排除其他更优解法比如 “两数之和”(无序数组),用哈希表是 O (n),比双指针(先排序 O (nlogn)+ 左右指针 O (n))更优,这时候就不用双指针;但如果是 “两数之和 II”(有序数组),双指针比哈希表更省空间(O (1) vs O (n)),这时候优先用双指针。
如果题目符合以下情况,大概率不用双指针:
- 暴力解法已经是 O (n) 或 O (logn)(比如 “找数组中的最大值”,直接遍历 O (n),不用双指针);
- 数组无序且排序后会破坏题目条件(比如 “找数组中两数之和的索引”,排序会丢索引,这时候用哈希表);
- 子数组问题中元素有正有负(滑动窗口的单调性被打破,比如 “和为 0 的子数组”,用前缀和 + 哈希表更优)。
对于滑动窗口的理解请看一下节
1074

被折叠的 条评论
为什么被折叠?



