滑动窗口
【优快云】滑动窗口详解
滑动窗口是一种双指针的方法。两个指针包含的中间的元素是一个窗口。
对于双指针,left和right,窗口的覆盖范围是: [ l e f t , r i g h t ) [left,right) [left,right)是一个左开右闭的区间。也就是说当left==right时,窗口大小是0。
重要的几个方面:
- 初始窗口的形成
- 窗口的更新和维护(两个指针的移动)
- 一般情况下,left和right都执行加操作
- 那么对left执行加,意味着有数据退出窗口
- 对right执行加,意味着有数据加入窗口
例题练习与分析
leetcode #76:最小覆盖子串
这是一个求解最小窗口的题目,应用滑动窗口的思路是:
- 初始化的窗口大小为0,执行第2步。
- 更新right指针,递增,直到刚刚能够满足题目要求,然后定死right,执行第3步。
- 更新left指针,递增,直到刚刚要使得窗口不能满足题目要求,意味着这是该right下最小的窗口了。此时判断并更新一下记录的最小满足的窗口大小以及窗口信息;再回到第2步。
如此,时间复杂度是: O ( 2 n + m ) O(2n+m) O(2n+m)其中n是s的长度,m是t的长度。
string minWindow(string s, string t) {
//就是要找一个最小的窗口
unordered_map<char, int> window;//记录当前窗口中已经覆盖的t中的字符
unordered_map<char, int> need; //记录还需要覆盖的t中的字符
for(int i=0;i<t.size();i++){
if(need.count(t[i])>0){
need[t[i]]+=1;
}
else{
need[t[i]]=1;
}
}
int left=0, right=0;
int start=0, min_size=s.size();
char cur;
int sat=0, cur_size=-1;
while(right<s.size()){
cur = s[right++];
if(need.count(cur)>0){
need[cur]-=1;
window[cur] = window.count(cur)>0 ? window[cur]+1 : 1;
if(need[cur]>=0)
sat+=1;//又满足了一个了
}
if(sat == t.size()){
//目前情况下t的全部都已经满足了
//然后需要尝试缩小窗口,缩小之后,left和right规定的窗口时刚好不满足的
while(left<=right){
cur = s[left++];
if(window.count(cur)>0 && window[cur]>0){
window[cur]-=1;
need[cur]+=1;
if(need[cur]==1){
sat-=1;
cur_size=right-left + 1;
// cout<<cur_size<<endl;
if(cur_size<min_size){
start=left-1;
min_size=cur_size;
}
break;
}
}
}
}
}
if(cur_size==-1){
return "";
}
return s.substr(start, min_size);
}
leetcode #438:找到字符串中所有字母异位词
异位词的概念:就是原字符串中字符随机打乱
很明显这是一个固定窗口大小的问题,窗口大小是p的长度。
所以在这里重要的是left的指针的更新,也可以倒着做,就是看right怎么更新了。
这里的时间复杂度呢?
O
(
m
+
m
n
)
O(m+mn)
O(m+mn)?大概是这样吧。
vector<int> findAnagrams(string s, string p) {
vector<int> result;
if(p.size()>s.size()) return result;
unordered_map<char, int> need;
unordered_map<char, int> window;
for(int i=0;i<p.size();++i){
need[p[i]] = need.count(p[i])>0?need[p[i]]+1:1;
}
int left=0, right;
char cur;
bool not_in=false, got_more=false;
int next_start=left, j, i;
while((right=left+p.size())&& right<=s.size()){
for(i=next_start;i<right;i++){
cur = s[i];
if(need.count(cur)==0){
not_in = true;
for(j=left;j<i;j++){
window[s[j]]=0;
}
left=i+1;
next_start=left;
break;
}
else{
if(window.count(cur)>0 && window[cur]==need[cur]){
got_more = true;
window[cur]+=1;
for(j=left;j<=i;++j){
window[s[j]]-=1;
if(window[cur]==need[cur]){
left=j+1;
next_start=right;
break;
}
}
}
else{
window[cur] = window.count(cur)>0?window[cur]+1:1;
}
}
}
if(!(not_in || got_more)){
result.push_back(left);
window[s[left]]-=1;
left++;
next_start = right;
}
not_in = false;
got_more = false;
}
return result;
}
leetcode #1658: 将 x 减到 0 的最小操作数
据说这个题也可以使用双指针来完成。
那么分析一下,这个题的结果实际上是一个最大的窗口,这个窗口位于数组的中间。
问题一:如何创建初始的窗口
这个题目,左右两边都需要减去一部分,那么初始的窗口可以是这样的:
先按照全部减去左边来做的结果,left的值设置为x可减的最大情况(可能会有剩余),然后right设置为数组长度
或者:可以从若干状态中先找一个减法较少的情况作为初始状态。
滑动窗口(双指针的方法)就像是人工智能里面的一种搜索一样,但它不像BFS/DFS是盲目的,倒有点像启发式搜索方法。
问题二:如何更新双指针
实际上,就是每次固定left,然后搜索right的值看有没有满足题意的,然后更新right无用的情况下,再去更新left。
最终求解的窗口范围: [ l e f t , r i g h t ) [left, right) [left,right)表示不减去这个窗口中的数字。
这个时间复杂度是: O ( n ) O(n) O(n)
int minOperations(vector<int>& nums, int x) {
//可以使用人工智能里面的搜索算法,这里返回最小操作数,可以使用广度优先搜索,就是一个二叉树
// return minO(nums, 0, nums.size()-1, x);//超出时间限制
// return minO_1(nums, x);//超出时间限制
int left=0, right=nums.size();
while(left<right && x>0){
x-=nums[left++];
if(x<0){
x+=nums[--left];
break;
}
}
if(left==right){
if(x>0){
//全加到一起都不行
return -1;
}
else{
return left;
}
}
int times=-1, temp;
if(x==0) times = left;
while(right >= left && left>=0){
while(x>0){
//更新right
temp=x-nums[right-1];
if(temp>0){
x=temp;
right-=1;
continue;
}
else if(temp==0){
x=temp;
right-=1;
temp = nums.size()-(right-left);
if(times==-1 || temp < times){
times=temp;
}
break;
}
else{
break;
}
}
//更新left,此时x>=0
--left;
if(left<0)break;
x+=nums[left];
}
return times;
}