算法思想
双指针其实是利用数组(或者求解问题)的有序特性,在遍历的过程中使用两个相同方向或者相反方向的指针进行扫描,从而达到目的的一种算法。
(注:这里的指针并非专指c语言中的指针,表达的含义是下标、索引值或者是可进行迭代的对象等)
算法模板
我们常见的一般的二重循环如下:
for(int i = 0; i < n; i ++) {
for(int j = 0; j < n; j ++) {
}
}
而双指针算法可以将上面的二重循环优化成这样(模板不唯一):
for(int i = 0, j = 0; i < n; i ++) {
while(j < i && check(i,j)) {
j++;
}
}
虽然看上去仍然是循环套循环,但是实际上是一个O(n)的时间复杂度。
为什么可以到达O(n)的时间复杂度呢?因为每一个指针在所有循环里面总共移动的次数是不超过n的,那么两个指针总共移动的次数不超过2n次。
算法应用
一般来讲常见的双指针算法分为两种:
(1) 对于一个序列,用两个指针维护一段区间,比如快排。
例题1 AcWing 799.最长连续不重复子序列(属于第一类)
思路:最开始让i、j指针均指向数组的开始位置,再移动i指针,当i指针已经无法再右移时(a[i]所表示的值已经在前面序列中出现过,用s数组判重处理),可获得一段下标为[j,i]的连续不重复子序列。再将j指针后移(此时while循环起作用),边移动j指针一边在s表示的数组中剔除a[j]表示的值,j指针移动到i指针位置时,便可进行下一段子序列的扫描。
代码:
#include
#include
using namespace std;
const int N = 100010;
int n;
int a[N];
int s[N]; // 当前j到i的区间里,每一个数出现的次数
// 可以不开数组判重,但需要使用hash表
int main() {
cin >> n;
for(int i = 0; i < n; i ++) {
scanf("%d", &a[i]);
}
int res = 0;
for(int i = 0, j = 0; i < n; i ++) {
s[a[i]] ++;
while(s[a[i]] > 1) { // 当a[i]值的个数大于1时
s[a[j]] --; // 剔除该数
j ++; // 右移j指针
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
例题2 AcWing 1238.日志统计(属于第一类)
思路:由题意可知,我们通过维护时间段的区间来进行双指针的算法。开始时i和j指针均为时间段的开始位置,随着时间的推移(i指针后移),往cnt数组里面添加id出现的次数,一旦大于k次,便用st数组将其记录为热帖。
代码:
#include
#include
#include
using namespace std;
typedef pair PII; // 需要对二元组排序时使用,默认以first为第一个关键字排序
const int N = 1e5 + 10;
int n, d, k;
PII logs[N];
int cnt[N];
bool st[N]; // 记录每个帖子是否是热帖
int main() {
cin >> n >> d >> k;
for(int i = 0; i < n; i ++) {
scanf("%d %d", &logs[i].first, &logs[i].second);
}
sort(logs, logs + n);
for(int i = 0, j = 0; i < n; i ++) {
cnt[logs[i].second] ++;
while(logs[i].first - logs[j].first >= d) { // id的时间段大于了题意的d
cnt[logs[j].second] --; // 剔除该id
j ++; // j指针后移
}
if(cnt[logs[i].second] >= k) {
st[logs[i].second] = true;
}
}
for(int i = 0; i <= 100000; i ++) {
if(st[i]) {
printf("%d\n", i);
}
}
return 0;
}
(后续题目待补充)
算法总结
可以先想一个暴力O(n^2)的做法,然后找i与j之间的单调关系再进行双指针优化。