【算法】滑动窗口
什么是滑动窗口算法?
滑动窗口算法本质上就是双指针的一种情况,当两个指针进行移动的方向是同一个方向,并且这两个指针并不会向后回退,一直是往一个方向进行移动的。这也就是滑动窗口的使用场景。
滑动窗口算法的一般步骤
进窗口
判断
出窗口
更新结果---这个更新结果有可能是在进窗口的时候进行更新结果,还有可能是在进行判断后进行更新结果,还有可能是在出窗口后进行更新结果,但是进窗口、判断、出窗口这三个的顺序是不变的,相当于将更新结果进行放到他们之间。
209、长度最小的数组
题解
注意:题目中说最长的子数组,必须是连续的
暴力
通过两个for循环进行解决,时间复杂度为O(n*n)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int count=INT_MAX;
for(int i=0;i<nums.size();i++)
{
int sum=0;
for(int j=i;j<nums.size();j++)
{
sum+=nums[j];
if(sum>=target)
{
count=min(count,j-i+1);
break;
}
}
}
return count==INT_MAX?0:count;
}
};
滑动窗口
滑动窗口就是通过暴力算法进行优化出来的的,当进行第二个for循环时,通过双指针的方式进行看待for循环,当两个for循环的和大于7时,进行记录下,然后将移动前的位置的值进行减去即可。从而优化了效率。
滑动窗口的实现
进窗口:将right对应位置的数组元素进行累加到sum中。
判断:出窗口条件的判断,累加的和大于目标值的时候。
更新结果:更新结果是根据题目中的要求进行的,这题中要求sum>=target也就是进行出窗口的判断条件,所以说应该先进行更新结果,然后在进行出窗口,因为一旦进行出窗口后,条件可能不满足。
出窗口:将left对应位置的数组元素从sum中进行移除。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int left=0;
int right=0;
long long sum=0;
int count=INT_MAX;
while(right<nums.size())
{
sum+=nums[right]; //进窗口
while(sum>=target) //判断
{
int temp=right-left+1;
if(temp<count)
{
count=temp; //更新结果
}
sum-=nums[left]; //出窗口
left++;
}
right++;
}
return count==INT_MAX?0:count;
}
};
3、无重复字符的最长字串
题解:
纯暴力:
如果进行采用暴力的形式进行解决这道题目的话,需要进行使用三个循环进行解决,其中前两层循环用于进行遍历在字符串,最后一层循环用于进行验证两个字符之间(包括这两个字符)是否出现了相同的字符。
暴力+哈希表:
通过两层循环进行遍历字符串中的每一个字符,然后不在进行通过循环的方式进行判断,而是将遍历的每一个字符进行丢到哈希表中进行判断,将时间复杂度直接从O(n*n*n)→O(n*n)
滑动窗口:
基于暴力进行优化
进窗口:将right对应的元素进行丢到哈希表中
判断:出窗口条件的判断,哈希表中存在两个相同字符
出窗口:将左侧的冲突元素进行移除
更新结果:出窗口后是满足题意条件的,所以说要在出窗口后进行更新结果。
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int left=0;
int right=0;
int count=-1;
unordered_map<char,int> hash;
while(right<s.size())
{
//进窗口
hash[s[right]]++;
while(hash[s[right]]>1) //判断
{
hash[s[left]]--;
left++; //出窗口
}
//更新结果
count=max(count,right-left+1);
right++;
}
return count==-1?0:count;
}
};
1004、最大连续1的个数
题解:
暴力:
通过遍历数组中的所有元素,通过引入变量进行就是反转0的个数。
滑动窗口:
首先要进行理解 这里的反转并不是一定要真的进行反转,而是进行模拟反转
进窗口:这道题和之前的题目是有所不同的,上面我们做的两道题的窗口是由两个指针进行维护,但是这道题的窗口是由题目条件反转0的个数进行维护的。两个指针用于进窗口条件的判断和进行更新结果。
判断:进行出窗口的条件,反转0的个数大于k
出窗口:将left指针进行移动到第一个0的位置的下一个。
更新结果:进行出窗口后一定是满足题目条件的,进行出窗口即可。
class Solution {
public:
int longestOnes(vector<int>& nums, int k)
{
int left=0;
int right=0;
int temp=0;
int count=0;
while(right<nums.size())
{
if(nums[right]==0)
{
temp++; //进行窗口
}
while(temp>k) //判断
{
if(nums[left]==0)
{
temp--; //出窗口
}
left++;
}
count=max(count,right-left+1); //进行更新结果
right++;
}
return count;
}
};
1658、将x减到0的最小操作数
题解
拿到这道题,通过阅读题意根本无从下手,因为情况太过于复杂,有的时候是全是左边的数,有的时候全是右边,最恶心的是操作数过多时两边都有的情况,这些情况简直太太太太多了,根本无从下手。当遇到这种正面无从下手的题目时,脑海里一定要飘过正难则反。
从反面进行思考问题
既然是正面进行考虑问题最难处理的点往往通过反面就可以进行解决。比如说这道题,从正面进行考虑都是需要获取数组两端的子数组,但是从反面进行考虑的话,直接就变成了中间的一块子数组,进行处理起来就是子树组问题了。
暴力
先进行问题转化,将数组进行求和,然后减去正面问题的目标值就是反面问题的目标值了,然后就转化成了子数组问题了,最长子数组问题。
然后通过两个for循环进行查找数组的最长值即可。
滑动窗口
进行优化,原理就和上面209题一样,就不再进行解释了
class Solution {
public:
int minOperations(vector<int>& nums, int x)
{
//将问题进行转化--正难则反思想
int sum=0;
for(int i=0;i<nums.size();i++)
{
sum+=nums[i];
}
int target=sum-x;
int left=0;
int right=0;
int temp=0;
int min_=INT_MAX;
while(right<nums.size())
{
temp+=nums[right]; //进窗口
while(left<=right&&temp>target) //判断
{
temp-=nums[left]; //出窗口
left++;
}
if(temp==target)
{
int n=nums.size()-(right-left+1);
min_=min(min_,n); //进行更新结果
}
right++;
}
return min_==INT_MAX?-1:min_;
}
};
904、水果成篮
题解:
暴力+哈希
哈希用于进行判断“篮子里”是否已经有了该元素,再有该同类水果时不需要进行额外占用篮子,否则需要进行额外占用篮子,通过哈希其实已经进行优化了一层遍历水果种类是否超过篮子个数的循环了。
滑动窗口
通过滑动窗口进行优化暴力。
这道题其实和最大连续1的个数这道题有异曲同工之妙,进行维护窗口是题目中的要求(通过引入变量进行维护的),并不是通过双指针进行维护的。
进窗口:以篮子进行维护窗口,当出现新品种水果时,需要进行增加篮子数
判断:出窗口条件,当篮子数不满足题意超过两个触发出窗口条件
出窗口:将水果从篮子里进行拿出,如果拿到最后一个拿出后,就腾出来一个篮子,这就是出窗口的操作。
更新结果:更新结果出现在满足题意的情况下。
class Solution {
public:
int totalFruit(vector<int>& fruits)
{
unordered_map<int,int> hash;
int target=0;
int left=0;
int right=0;
int count=-1;
while(right<fruits.size())
{
if( hash[fruits[right]]==0)
{
target++; //进窗口
}
hash[fruits[right]]++;
while(target>2) //判断
{
hash[fruits[left]]--;
if(hash[fruits[left]]==0)
{
target--; //出窗口
}
left++;
}
//进行更新结果
count=max(count,right-left+1);
right++;
}
return count;
}
};
438、找到字符串中所有字母的异位词
题解
暴力+哈希
首先先将字符串p中的字符进行放入哈希表中,然后通过两层for循环将字符串s中的部分字符进行放入另一个哈希表中,然后进行比对两个哈希表中的元素是否相同。
滑动窗口
通过滑动窗口进行优化
进窗口:将元素进行放入哈希表中
判断:放入的指定字符串中的字符超过目标字符串的长度
出窗口:将left位置的字符进行移除哈希表
更新结果:出窗口后就是满足长度的条件,通过判断两个哈希表是否相同进行更新结果
class Solution {
public:
vector<int> findAnagrams(string s, string p)
{
unordered_map<char,int> hash;
for(int i=0;i<p.size();i++)
{
hash[p[i]]++;
}
int left=0;
int right=0;
vector<int> ret;
unordered_map<char,int> temp;
while(right<s.size())
{
temp[s[right]]++; //进窗口
while(right-left+1>p.size()) //判断
{
temp[s[left]]--; //出窗口
left++;
}
//更新结果
for(int i=0;i<hash.size();i++)
{
if(hash[i]!=temp[i])
{
break;
}
if(i==hash.size()-1)
{
ret.push_back(left);
}
}
right++;
}
return ret;
}
};
30、串联所有单词的子串
题解
这道题和上一道题找到所有字符串中的异位词十分相似,只不过找到所有字符串中的异位词是进行查找的是字符,这道题进行查找的是字符串。
暴力
通过暴力进行解决这个问题,和上面思路相同,直接通过两个for循环进行嵌套进行暴力枚举,然后通过哈希表和变量进行判断是否符合条件,具体的判断思路是先将words中的字符串进行丢的哈希表1中,然后通过遍历将字符串进行扔到第2个哈希表中,并且第二个哈希表中进行存放的元素必须是在第一个哈希表中存在的,如果存在的个数小于哈希表1中的,通过计数器进行++,这个计数器是用于记录什么时候哈希表1中的字符串哈希表2中全存在的,这时候需要进行更新结果。
滑动窗口
通过滑动窗口进行优化,使用滑动窗口时,需要进行时刻谨记两个指针全是正向移动的,不能出现回退的现象。
细节处理:
这道题仅仅使用滑动窗口来进行解决问题是会进行漏掉情况的,例如上图情况,所以需要在滑动窗口外面再进行套一层循环,但是这个循环不需要是整个s字符串的长度,只需要是word种一个字符串的长度即可,因为当循环word种一个字符串的长度时,就将所有错开的情况全部覆盖到了。
进窗口:将字符串进行丢入的第二个哈希表种
判断条件:当right和left之间的距离超过word种字符的总长度
出窗口:将最前面的字符串从哈希表2种进行弹出
更新结果:需要包含word中的任意字串时进行更新结果
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words)
{
unordered_map<string, int> hash1;
vector<int> ret;
for (int i = 0; i < words.size(); i++)
{
hash1[words[i]]++;
}
for (int i = 0; i < words[0].size();i++)
{
int left = i;
int right = i;
int count=0;
unordered_map<string, int> hash2;
int len=words[0].size();
while (right+len<=s.size())
{
string in=s.substr(right,len);
//进窗口
hash2[in]++;
if(hash2[in]<=hash1[in])
{
count++;
}
//判断
if(right-left+1>len*words.size())
{
string out=s.substr(left,len);
if(hash2[out]<=hash1[out])
{
count--;
}
//出窗口
hash2[out]--;
left+=len;
}
//更新结果
if(count==words.size())
{
ret.push_back(left);
}
right+=len;
}
}
return ret;
}
};
76、最小覆盖字串
题解
class Solution {
public:
string minWindow(string s, string t)
{
int left=0;
int right=0;
unordered_map<char,int>hash1;
unordered_map<char,int>hash2;
int count=0;
int begin=0;
int minlen=INT_MAX;
string ret;
for(int i=0;i<t.size();i++)
{
hash1[t[i]]++;
}
while(right<s.size())
{
//进窗口
hash2[s[right]]++;
if(hash1.count(s[right])&&hash2[s[right]]==hash1[s[right]])
{
count++;
}
//判断
while(count==hash1.size())
{
//进行更新结果
if(right-left+1<minlen)
{
minlen=right-left+1;
begin=left;
}
//出窗口
hash2[s[left]]--;
if(hash1.count(s[left])&&hash2[s[left]]<hash1[s[left]])
{
count--;
}
left++;
}
right++;
}
ret=s.substr(begin,minlen);
return minlen == INT_MAX ? "" : s.substr(begin, minlen);
}
};