目录
学习目标
- 了解双指针算法是什么以及分类
- 理解双指针算法的原理
- 会用代码编写双指针算法
- 在实际题目中灵活运用双指针
📌在数组的开章中我们提到了这个算法,如果没有看的话可以学完这一节再回头去看那一篇文章会恍然大悟!!
什么是双指针?
用最通俗的语言来讲所谓双指针算法是针对于遍历的过程的,我们平常在用for循环遍历的时候都是用单个指针在进行循环访问,而双指针就是用两个相同方向的或者相反方向的指针进行扫描,从而达道算法的目的。比如在一层循环里同时设置 i,j两个变量。
换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算,将时间复杂度从log(n^2)降到log(n)。
注意:这里的指针,并非专指C语言中指针的概念,而是指索引,游标或指针,可迭代对象等!
双指针的分类
- 若两个指针指向同一数组,遍历方向相同且不会相交,则称为滑动窗口,用于解决区间搜索问题。
- 若两个指针指向同一数组,遍历方向相反,则可以用来搜索有序数组(需要提前进行排序)。
- 还有快慢指针,可以用来判断链表环路问题。
核心思想
有重复计算的区间,用双指针优化。将上一个状态指针所表达的信息传递至下一状态,减少无谓的搜索。
模板写法
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
// 这是i,j分别两端的写法
}
//这是i,j分别两端的写法
for(int i=0,j=n-1;i<j;i++){
while(check()) j--;
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
常用于解决:数组、区间、字符串等问题
经典例题
移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
双指针法
分析题意
首先来分析一下这道题为什么可以使用双指针法,很明显这是一道考察数组的题,我们上面说了数组的题可以考虑用双指针的解法,那么还有其他原因吗?双指针可以代替本来暴力写法的两层循环(不知道暴力学法怎么写的可以参考数组开章的那篇博客),从而减小时间复杂度,答案优化算法的目的。
📌当然我们必须先思考出暴力写法应该如何写,才能想到双指针的优化。
具体代码
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slow=0;//双指针 慢指针为长度
for(int fast=0;fast<nums.size();fast++){
if(nums[fast]!=val){
nums[slow]=nums[fast];
slow++;
}
}
return slow;//返回新的数组的下标
}
};
最长连续不重复子序列
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n 个整数(均在 0∼10^5范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^{5} 1≤n≤105
输入样例:
5
1 2 2 3 5
输出样例:
3
核心思路
本题使用i,j两个指针来维护一个区间,如果区间内没有重复的数字就让i指针一直向前移动并且用s[]数组来记录a[]数组中每个元素出现的次数,每出现一个重复的数字的时候就移动j和i重合,重新开始计算区间的长度,最后去更新一个最大的区间长度。
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],s[N];
int n;
//双指针:找i和j存在什么规律:单调性
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int res=1;
for(int i=1,j=1;i<=n;i++)
{
s[a[i]]++;//注意这一步的位置,向右扩展右端点
//当扩展完区间右端点之后,有可能这个元素q[i]会有重复,下面这个while循环就是用来去除重复
//去重只有一个办法,就是收缩区间左端点,同时收缩时要保证j是小于i的
while(j<i&&s[a[i]]>1)//如果执行这个循环,结束后i与j重合
{
s[a[j]]--;
j++;
}
res=max(res,i-j+1);
}
cout<<res<<endl;
return 0;
}
数组元素的目标和
给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x的数对 (i,j)。
数据保证有唯一解。
输入格式
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。
第二行包含 n个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。
输出格式
共一行,包含两个整数 i和 j。
数据范围
数组长度不超过 10^5
同一数组内元素各不相同。
1≤数组元素≤10^9
输入样例:
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:
1 1
核心思路
i从 0开始 从前往后遍历
j从 m - 1开始 从后向前遍历
📌和纯暴力的O(n2)算法的区别就在于:j指针不会回退
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m, x;
int a[N], b[N];
int main()
{
scanf("%d%d%d", &n, &m, &x);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);
//设置前后两个指针 去更新j指针
for (int i = 0, j = m - 1; i < n; i ++ )
{
while (j >= 0 && a[i] + b[j] > x) j -- ;
if (j >= 0 && a[i] + b[j] == x) cout << i << ' ' << j << endl;
}
return 0;
}
总结一下
本篇我们学习了双指针算法,可以回头看一下学习目标大家有没有达成了呢~
从例题中可以看到这些题都有一个共性就是都提到了数组这一概念,也就是上一篇数组博客
的内容理解透彻以后再来看这一篇会更加清晰一些😀
创作不易,喜欢的话动动小手点个免费的赞啦,谢谢大家!!~😀💜❤️