前言
本篇为滑动窗口算法核心代码模板 | labuladong 的算法笔记的笔记。
滑动窗口主要用来解决子数组问题,比如让你寻找符合某个条件的最长/最短子数组
基于滑动窗口算法框架写出的代码,时间复杂度是 O(N)
,比嵌套 for 循环的暴力解法效率高。
滑动窗口模版
/* 滑动窗口算法框架 */
void slidingWindow(string s) {
// 用合适的数据结构记录窗口中的数据,根据具体场景变通
// 比如说,我想记录窗口中元素出现的次数,就用 map
// 我想记录窗口中的元素和,就用 int
unordered_map<char, int> window;
int left = 0, right = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
window.add(c)
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
// 注意在最终的解法代码中不要 print
// 因为 IO 操作很耗时,可能导致超时
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (left < right && window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
window.remove(d)
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
六道题讲解模版
第一题
一开始我的想法是这样的,但是会TLE(我的判断函数是针对字符串t中的每个元素,比对hmap_tt和hmap_s中的值):
class Solution {
public:
bool judge(unordered_map<char,int> hmap_s,unordered_map<char,int> hmap_tt,string t,int len_tt){
for(int i=0;i<len_tt;i++){
if(hmap_tt[t[i]]>hmap_s[t[i]]){
return false;
}
}
return true;
}
string minWindow(string s, string t) {
unordered_map<char,int> hmap_s;
unordered_map<char,int> hmap_tt;
string res="";
int res_len=INT_MAX;
int len_s=s.size();
int len_tt=t.size();
int l=0;
int r=0;
int res_l=0;
int res_r=0;
for(int i=0;i<len_tt;i++){
hmap_tt[t[i]]++;
}
while(r<len_s){
char temp=s[r];
r++;
hmap_s[temp]++;
while(l<=r && judge(hmap_s,hmap_tt,t,len_tt)){
if(res_len>r-l){
res_len=r-l;
res_l=l;
res_r=r;
}
char cnt=s[l];
l++;
hmap_s[cnt]--;
}
}
if(res_len==INT_MAX){return "";}
return ""+s.substr(res_l,res_len);
}
};
但是,我们可以使用备忘录:valid
变量表示窗口中满足 need
条件的字符个数,如果 valid
和 need.size
的大小相同,则说明窗口已满足条件。修改代码如下:
class Solution {
public:
bool judge(int valid,int len_tt){
if(valid>=len_tt)
return true;
else
return false;
}
string minWindow(string s, string t) {
unordered_map<char,int> hmap_s;
unordered_map<char,int> hmap_tt;
string res="";
int res_len=INT_MAX;
int len_s=s.size();
int len_tt=t.size();
int l=0;
int r=0;
int res_l=0;
int res_r=0;
int valid=0;
for(int i=0;i<len_tt;i++){
hmap_tt[t[i]]++;
}
while(r<len_s){
char temp=s[r];
r++;
hmap_s[temp]++;
if(hmap_s[temp]<=hmap_tt[temp]){valid++;}
while(l<=r && judge(valid,len_tt)){
if(res_len>r-l){
res_len=r-l;
res_l=l;
res_r=r;
}
char cnt=s[l];
l++;
hmap_s[cnt]--;
if(hmap_s[cnt]<hmap_tt[cnt]){valid--;}
}
}
if(res_len==INT_MAX){return "";}
return ""+s.substr(res_l,res_len);
}
};
第二题
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
这道题该怎么应用滑动窗口呢(题目说了 nums
中的元素都是正数,这就保证了只要有元素加入窗口,和一定变大,只要有元素离开窗口,和一定变小;负数的话就没有这个性质了,也就不能确定什么时候扩大和缩小窗口,也就不能使用滑动窗口算法,那应该使用什么方法呢?前缀和+哈希,后面会进行讨论
题目让你从边缘删除掉和为
x
的元素,那剩下来的是什么?剩下来的是不是就是nums
中的一个子数组?让你尽可能少地从边缘删除元素说明什么?是不是就是说剩下来的这个子数组大小尽可能的大? 这道题等价于让你寻找nums
中元素和为sum(nums) - x
的最长子数组。
代码如下:
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int len=nums.size();
int sum=0;
for(int i=0;i<len;i++){sum+=nums[i];}
int temp_sum=sum;
sum-=x;
int l=0;int r=0;int cnt=0;int res=0;
while(r<len){
int temp=nums[r];
r++;
cnt+=temp;
while(l<r && cnt>=sum){
int temp_l=nums[l];
if(r-l>res && cnt==sum){res=r-l;}
l++;
cnt-=temp_l;
}
}
res=len-res;
if(res==len && temp_sum!=x ){return -1;}
return res;
}
};
现在讨论元素存在负数的情况:
首先先来看一下这道题(采用前缀和+哈希的方式来解决):560. 和为 K 的子数组 - 力扣(LeetCode)
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
int len=nums.size();
//对于数组中的每一个元素x,只要在哈希表中,包含x的前缀和y-k存在,即这种情况成立
vector<int> g(len+1,0);
unordered_map<int,int> hmap;
int res=0;
g[0]=0;
hmap[0]=1;
for(int i=0;i<len;i++){
g[i+1]=nums[i]+g[i];
if(hmap.find(g[i+1]-k)!=hmap.end()){
res+=hmap[g[i+1]-k];
}
hmap[g[i+1]]++;
}
return res;
}
};
现在再尝试用哈希和前缀和的思想来解决第二题:
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int len=nums.size();
int sum=0;
for(int i=0;i<len;i++){sum+=nums[i];}
int temp_sum=sum;
sum-=x;
vector<int> g(len+1,0);
unordered_map<int,int> hmap;
hmap[0]=0;
int res=0;
for(int i=0;i<len;i++){
g[i+1]=g[i]+nums[i];
if(hmap.find(g[i+1]-sum)!=hmap.end()){
res=max(res,i+1-hmap[g[i+1]-sum]);
}
if(hmap.find(g[i+1])==hmap.end()){
hmap[g[i+1]]=i+1;
}
}
if(res==0 && sum!=0){return -1;}
return len-res;
}
};
不得不想到软微笔试第二题:给定数组A,数组长度N,以及数S,求得满足和>S的子数组最小长度,需要满足小于O(NlogN)的时间复杂度。前缀和+哈希表无法解决这道题是因为满足和>S而非=S。【需要拓展怎么求解】
第三题
1004. 最大连续1的个数 III - 力扣(LeetCode)
在可以修改字符的条件下寻找符合条件的子数组,也可以用滑动窗口算法
代码如下:
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int len=nums.size();
vector<int> g;
g=nums;
int l=0;int r=0;int res=0;int temp=0;
while(r<len){
if(nums[r]==0 && k>0){
nums[r]=1;
k--;
temp++;
}
else if(nums[r]==1){
temp++;
}
res=max(res,temp);
r++;
if(r<len && r-1>=0 && nums[r-1]==1 && nums[r]==1){continue;}
while(l<r && k==0){
if(g[l]==0 && nums[l]==1){
nums[l]=0;
k++;
}
if(temp>0){temp--;}
l++;
}
}
return res;
}
};
第四题
进阶版的第三题:424. 替换后的最长重复字符 - 力扣(LeetCode)
要处理很多的小细节,循环26个字母 i ,依次判断替换成字母 i 后(有 k 次机会)的最长重复字符长度。
代码如下:
class Solution {
public:
int characterReplacement(string s, int k) {
int len=s.size();
string ss=s;
int res=0;
char standard;
int l,r,temp_res,temp_k;
for(int i=0;i<26;i++){
standard=i+'A';
l=0,r=0;
temp_res=0;
temp_k=k;
while(r<len){
char temp=s[r];
if(temp!=standard && temp_k>0){
s[r]=standard;
temp_k--;
}
r++;
if(r<len && s[r]==standard && s[r-1]==standard){res=max(res,r-l+1);continue;}
while(l<r && temp_k==0){
res=max(res,r-l);
char num=s[l];
if(num!=ss[l]){
s[l]=ss[l];
temp_k++;
}
l++;
}
}
s=ss;
}
return res;
}
};
第五题
在指定大小的子数组中寻找符合条件的元素,也可以用到滑动窗口算法。
这题考察滑动窗口技巧,你维护一个大小为
k
的滑动窗口滑过整个数组,滑动的过程中计算窗口中是否存在重复元素。class Solution { public: bool containsNearbyDuplicate(vector<int>& nums, int k) { int len=nums.size(); k=min(len-1,k); int l=0,r=k; unordered_set<int> hset; for(int i=0;i<k;i++){ if(hset.find(nums[i])!=hset.end()){return true;} hset.insert(nums[i]); } while(r<len){ int temp=nums[r]; if(hset.find(temp)!=hset.end()){return true;} else{ hset.insert(temp); } r++; if(l<r){ hset.erase(nums[l]); l++; } } return false; } };
第六题
220. 存在重复元素 III - 力扣(LeetCode)
abs(i - j) <= indexDiff 滑动窗口信号
abs(nums[i] - nums[j]) <= valueDiff
数据结构(set)维护滑动窗口内的元素:
支持添加和删除指定元素的操作,否则我们无法维护滑动窗口;
内部元素有序,支持二分查找的操作(lower_bound函数),这样我们可以快速判断滑动窗口中是否存在元素满足条件。
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int indexDiff, int valueDiff) {
int len=nums.size();
int l=0,r=0;
set<long> s;
while(r<len){
int temp=nums[r];
auto find=s.lower_bound(long(temp));
if(find!=s.end() && *find-temp<=valueDiff){return true;}
if(find!=s.begin()){
find--;
if(temp-*find<=valueDiff){return true;}
}
s.insert(temp);
r++;
while(l<r && r-l>indexDiff){
s.erase(nums[l]);
l++;
}
}
return false;
}
};