滑动窗口
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目只有题目分析,代码实现,代码误区
题目描述:
-
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。第二行包含 n 个整数(均在 0∼105 范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。数据范围
1≤n≤105
输入样例:
5
1 2 2 3 5
输出样例:
3 -
题目来源:https://www.acwing.com/problem/content/801/
题目分析:
-
要求:最长&连续&不重复,非常常见的要求,没有接触过的同学应该记下他们的含义:
最长:我们需要用一个临时变量去记录原本最长序列的长度,再与当前序列长度相比,决定是否要替换 连续:中间某一点不满足条件,则需要舍弃这一点与之前的所有序列,下一点又从“头”做起 如:12235中,第一个2不满足不重复,删掉第一个2时,1也不可取,从第二个2重新开始时最大长度从1记录 不重复:需要记录一个数是否已经出现过 记录一个数出现次数可以开一个标记数组,相当于实现一个简易Hash
-
每个整数都在0-105的范围内,刚好开一个arr[110]数组即可完成每个数出现次数的记录
-
对于区间问题,双指针算法是经常采用的算法
今后遇到字符串问题,序列问题,都需要考虑“使用双指针试试”
下面我们来介绍双指针算法
算法原理:
含义:
- 双指针:就是两个临时变量,一个i,一个j。
之所以叫指针,是因为我们经常使用这两个变量来记录数组或字符串下标
作用:
- 双指针的作用很多,具体问题具体分析,但是其实做几道双指针的题目你基本就见全了所有使用情况
已经见过的双指针:
1. 单串双指针-快速排序:
- 确定一个基准数x之后,我们使用双指针完成将子序列中大于x的数放在右边,小于x的数放在左边
- 此处的双指针 i j 用于在x的左右两侧进行比较和交换
void q_sort(int l, int r){
if(l >= r) return;
int x = l+r>>1;
int i = l-1, j = r+1;
while(i < l){
while(arr[++i] < x);
while(arr[++j] > x);
if(i < j) swap(arr[i], arr[j]);
}
q_sort(l, j);
q_sort(j+1, r);
}
有点遗忘快排的同学看这里:传送门
2. 双串双指针-归并排序:
- 有序合并两个子序列到更大的序列中时,我们用i指针遍历左子序列,j指针遍历右子序列,让所合成的大序列变得有序
- 当将辅助记录数组tmp中的元素复制到原数组arr中时,我们用i指针从l到r遍历arr数组,用j指针从0遍历tmp数组,由于复制中的数组长度相同,所以终止条件只有i<=r
const int N = 100010;
int tmp[N];
void merge_sort(int l, int r){
if (l >= r) return;
int mid = (l + r)>>1;
merge_sort(l ,mid);
merge_sort(mid+1, r);
int i = l, j = mid+1, k = 0;
// 此处的双指针i j用于比较两个不同的子序列
while(i <= mid && j <= r){
if (arr[i] < arr[j]) tmp[k++] = arr[i++];
else tmp[k++] = arr[j++];
}
while(i <= mid) tmp[k++] = arr[i++];
while(j <= r) tmp[k++] = arr[j++];
//此处的双指针i j用于复制粘贴
for(int i=l, j=0; i<=r; i++,j++)
arr[i] = tmp[j];
}
- 忘记归并排序的同学看这里:传送门
3. 双串双指针-逆序对个数:
- 逆序对个数基于归并排序,而且对双指针的利用达到了极致,属于双指针的启发应用
- i指针所遍历的子序列本身在j指针遍历的子序列左边,
若此时arr[i] > arr[j],则构成一对逆序对
又由于左右子序列都有序,所以arr[i]及其右边有mid-i+1个元素都与arr[j]构成逆序对
const int N = 100010;
int tmp[N];
int merge_sort(int l, int r){
if (l >= r) return;
int mid = (l + r)>>1;
int res = 0;
res += merge_sort(l ,mid);
res += merge_sort(mid+1, r);
int i = l, j = mid+1, k = 0;
// 此处的双指针i j用于比较两个不同的子序列
while(i <= mid && j <= r){
if (arr[i] <= arr[j]) tmp[k++] = arr[i++];
else{
tmp[k++] = arr[j++];
res += mid-i+1;
}
}
while(i <= mid) tmp[k++] = arr[i++];
while(j <= r) tmp[k++] = arr[j++];
//此处的双指针i j用于复制粘贴
for(int i=l, j=0; i<=r; i++,j++)
arr[i] = tmp[j];
return res;
}
- 忘记归并统计逆序对个数的同学看这里:传送门
滑动窗口模型:
-
本题属于双指针中的滑动窗口问题,是双指针的典型应用
-
滑动窗口:一个快指针j负责遍历,慢指针i负责处理快指针遗留的问题
快指针j的向后移动是由于循环++
慢指针i的向后移动是由于i-j之间出现问题,为了消除问题,i指针++ -
举例:
初始i == j == 0,直到j指针移动到第二个8前,i-j之间没有重复元素,i一直没有移动,最大长度为4当j指针移动到第二个8时,i-j之间出现重复元素8,i指针通过++右移的方式来消除一个重复元素,此时i移动到第二个8,和j位置相同,目前长度为1<4
最终j移动到了数组结尾,期间i-j一直没有出现问题,此时i保持在第二个8不动,目前长度为3<4 -
滑动窗口的核心就在于两句话:
快指针负责遍历序列制造问题,慢指针负责解决问题。
快指针j++是由于遍历迫使,慢指针i++是由于消除问题
写作步骤:
两步:
构造滑动窗口:
int i = 0, j = 0;
int res = 0;
for(j=0; j<n; j++){
while(check()){
i++;
}
res = max (res, j-i+1);
}
判断问题出现:
- 也就是check()函数的构造,此处是一个book[]数组
代码模板:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int arr[N];
int Hash[N];
int main(){
int n = 0;
cin >>n;
for(int i=0; i<n; i++) cin >>arr[i];
int i = 0, j = 0;
int maxx = 0;
for(j = 0; j<n; j++){
Hash[arr[j]]++;
while(Hash[arr[j]]>1){
Hash[arr[i]]--;
i++;
}
maxx = max(maxx, j-i+1);
}
cout <<maxx;
}
代码误区:
1. 易漏点-连锁影响:
- i++的连锁影响:Hash[arr[i]]–;
- j++的连锁影响:Hash[arr[j]]++;
2. 手动构建的Hash存储范围:
- 题目明确告知所有数据在0 - 105,一方面非负,另一方面上限小,所以可以开数组存储
- 当数据存在负数或者范围非常大的时候,可以使用STL内置的map,但是有可能会被有些出题人卡STL内置的表长
- 后续的神机百炼中我会教大家飞速搭建自己的哈希表,肯定不会被卡,且时间复杂度还是O(1)
本篇感想:
- 由于本篇是双指针第一篇,所以虽然题目是滑动窗口,但是介绍了很多双指针hh(狗头)
- 看完本篇博客,恭喜已登《练气境-初期》
距离登仙境不远了,加油