🎆滑动窗口思想
🎇算法简介
- 定义:滑动窗口本质上就是基于双指针的一种算法思想,双指针之间的距离形成了一个窗口。一般窗口有两种类型,窗口是固定的或者窗口是动态变化的。左右两端两个指针在同一方向,向前进行滑动。
那么如何实现滑动窗口呢? - 很简单,只要定义 i 和 j 用来表示滑动窗口的左边界以及右边界。而我们通过改变 i 或者是 j 的值来扩大或缩小滑动窗口。
- 开始时,我们先推动右边界往右走,到达一个边界使得窗口内的数据满足我们的要求。这时候我们开始固定右边界,开始移动左边界,直到窗口内的数据刚好不满足我们的要求。
- 然后循环往复这个过程就可以了。
- 滑动窗口的解题中最重要的也是最难的点就在于对窗口的更新和维护。 因为往往在维护和更新窗口的时候,都需要借助其他数据结构和算法思想来操作。
🎇适用范围
- 滑动窗口可以很好的解决数组或列表的问题。如果一个问题可以通过不断的暴力循环直接解决,那么借助滑动窗口就可以把
O(n^2) 这种时间复杂度降低到 O(n) 。 - 一个很简单的例子,就是求一串数组当中连续5个数最大和。
- 或是当你遇到题目要求在一串大的数组、字符串当中求最大值,最小值,子序列这种时,就可以尝试使用滑动窗口去求解答案。
🎆例题1.无重复字符的最长字串
-
接下来让我们来看看几个例题。
-
这题是一个很典型的利用滑动窗口的题目,同样的我们需要先想清楚如何去维护以及更新这个窗口。
-
从提示当中我们可以很清楚的看出字符串s是由什么组成的,那么根据这个我们可以建立一个足够大的数组p来对应每一个字母。用数组p来维护和更新这个窗口,用0和1来表示窗口中是否出现当前字符,出现就把当前字符对应的数组元素变为1。
-
刚开始,我们固定左边窗口,对右边窗口进行移动,当我们判断子串之中已经出现过这个字符后。这时,固定右边窗口,开始移动左边窗口。
-
如果左边窗口的字符不等于右边窗口,那么将这个字符在p数组中的1变为0 (表示当前字符移出了窗口) ,相同则左边窗口在当前 i 位置的下一个停住,同时不改变这个字符在p数组中的1。以此反复,通过加减number和max作比较,得出答案。
int lengthOfLongestSubstring(char * s){
int length=strlen(s);//要求字符串的长度
//两种特殊情况,没有字符或只有1个。
if(length==0)return 0;
if(length==1)return 1;
int i=0,max=0,j=0;
int number=0;
int p[256]={0};//建立一个可以存放所有字符的数组(用ASCII码来对应)
for(j;j<length;j++){
if(p[s[j]]==0){//窗口内没有当前数
p[s[j]]=1;
number++;
}else {//证明这个位置在窗口内已经有数
if(number>max)max=number;
while(s[i]!=s[j]){
p[s[i]]=0;
i++;
number--;
}
i++;//找到重复了,就只改变一个i的位置就行。
}
}
if(number>max)return number;//最后一遍可能因为没有重复的,直接就退出循环,做个比较。
return max;
}
🎆例题2.最小覆盖字串
- 通过对上一题的简单了解,我们应该初步了解了如何运用滑动窗口。让我们更深刻的去理解这个方法。
- 第一步很显然这道题的基本步骤也是滑动窗口,首先我们不断增加 j 使滑动窗口扩大,直到窗口包含了T的所有元素。
- 第二步,我们就可以开始增加 i 使窗口缩小,减小长度。减小到第一个包含在t子串中的字符,这个时候我们记录滑动窗口的长度,并拿它跟满足条件的最小值进行比较。
- 第三步将当前字符移出窗口,然后固定左边界i,然后继续移动右边界,去寻找满足条件的窗口,如此反复。
- 思路依旧是滑动窗口,而且很容易理解,但困难的地方仍然在于如何维护以及更新窗口。
维护和更新窗口:
- 1. 我们需要建立一个字典,也就是上一题的数组,我们先对字符串t做一遍遍历,将t中的字符在字典中所对应的值进行++的操作。
- 2.在接下来字符进入窗口后,我们就将字符对应元素的值-1,字符移出窗口,就将字符对应元素的值 +1 。为什么进入窗口是减1而移出窗口是+1呢。因为这样做保证所有不包含在t中的字母,无论如何他们对应的值都小于等于0.(必须先进入窗口,才能出去)
- 3. 那么只要我们保证整个字典内的所有的值都小于或等于0时,则代表t中的字符已经被全部包含了。
先定义我们需要的数据:
int lengthS=strlen(s);
int lengthT=strlen(t);
int flag=0;
int minlength=lengthS;//包含t的最短子串的长度
int count=0;//用来记录字典中所有值的和(当count<=0时,代表满足包含t)
int dictionary[257]={0};//进入窗口就-- 离开窗口就++ 只有子串内的字符会大于0
int i=0,j=0;//左右窗口
int mini=0,minj=0;//最短子串下的左右窗口
- 定义中,因为我们minlength初始化的值是字符串S的长度,所以在最后的判断中可能会报错,通过flag可以判断是否满足过条件
排除特殊情况:
if(lengthS<lengthT)return "";//t长的特殊情况直接返回空字符串
if(lengthT==1){//t只有1个的情况直接遍历一遍返回答案
for(int i=0;i<lengthS;i++){
if(s[i]==t[0])return t;
}
return "";
}
初始化字典:
for(i=0;i<lengthT;i++){
dictionary[t[i]]++;//对字典进行初始化
count++;
}
- count初始化为t中字符串的个数,在进入窗口的数据中,如果是缺少的,我们就对count–。如果不是缺少的我们就不对count进行操作。如果我们需要的字符被移出窗口就进行++。
- 判断这个数是不是我们需要的就看他在字典对应的值是否大于0。
开始移动窗口:
if(dictionary[s[0]]>0){
count--;
}
dictionary[s[0]]--;//先将第一个字符加入窗口,并进行判断
while(i<lengthS){
if(count>0&&j==lengthS-1){//在不包含子串的情况,但是右窗口已经移动到字符串最右侧时
break;
}
else if(count>0){//不包含子串的情况,继续扩大右边窗口
j++;
if(dictionary[s[j]]>0){//证明进入就是当前所需的字符
count--;
}
dictionary[s[j]]--;
}
else{//包含子串的情况,需要移动左边窗口
if(j-i+1<=minlength){
minlength=j-i+1;//维护最小子串
mini=i;
minj=j;
flag=1;
}
//开始移动左边窗口
dictionary[s[i]]++;
if(dictionary[s[i]]>0)count++;
i++;
}
}
上题详细代码:
char * minWindow(char * s, char * t){
int lengthS=strlen(s);
int lengthT=strlen(t);
int flag=0;
if(lengthS<lengthT)return "";//t长的特殊情况直接返回空字符串
if(lengthT==1){//t只有1个的情况直接遍历一遍返回答案
for(int i=0;i<lengthS;i++){
if(s[i]==t[0])return t;
}
return "";
}
int minlength=lengthS;//维护包含t的最短子串
int count=0;//用来记录字典中所有值的和(当count<=0时,代表满足包含t)
int dictionary[257]={0};//进入窗口就-- 离开窗口就++ 只有子串内的字符会大于0
int i=0,j=0;//左右窗口
int mini=0,minj=0;//最短子串下的左右窗口
for(i=0;i<lengthT;i++){
dictionary[t[i]]++;//对字典进行初始化
count++;
}
i=0;
if(dictionary[s[0]]>0){
count--;
}
dictionary[s[0]]--;//先将第一个字符加入窗口,并进行判断
while(i<lengthS){
if(count>0&&j==lengthS-1){//在不包含子串的情况,但是右窗口已经移动到字符串最右侧时
break;
}
else if(count>0){//不包含子串的情况,继续扩大右边窗口
j++;
if(dictionary[s[j]]>0){//证明进入就是当前所需的字符
count--;
}
dictionary[s[j]]--;
}
else{//包含子串的情况,需要移动左边窗口
if(j-i+1<=minlength){
minlength=j-i+1;//维护最小子串
mini=i;
minj=j;
flag=1;
}
//开始移动左边窗口
dictionary[s[i]]++;
if(dictionary[s[i]]>0)count++;
i++;
}
}
if(minlength!=lengthS||flag==1){
char*p=(char*)malloc(sizeof(char)*minlength+1);
*p='\0';
strncat(p,s+mini,minlength);
return p;
}
return "";
}
✨总结
- 滑动窗口作为一种较为基础的思想,它的难点在于和其他数据结构的结合,你往往为了维护窗口,需要运用哈希,队列,字典来帮助你。再接下来的学习要加强对这方面知识的完善。