滑动窗口二
- 1.水果成篮
- 2.找到字符串中所有字母异位词
- 3.串联所有单词的子串
- 4.最小覆盖子串
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.水果成篮
题目链接:904. 水果成篮
题目分析:
这道题描述很多,主要意思是从左往右有一排树,给你两个篮子,每个篮子只能装一种类型水果。你可以任选从某一颗数开始摘水果,并且一棵树必须要摘一个水果。也就是说不能跳着摘水果。当你走到某棵树面前,但树上水果与你两个篮子水果类型不一样,就不能在继续了。下面就是找的两种情况。
根据上面的理解,这道题我们可以转化一下:
找出一个最长的子数组的长度,并且子数组中不超过两种类型的水果
算法原理:
首先要想到暴力求解,两层for循环,但是这里要求一个子数组中水果不能超过两种类型,对于这种不能有重复的元素或者这道题的意思,我们可以使用哈希表,因此第一种解法出来了。
解法一:暴力求解+哈希表
定义两个指针left=0,right=0,当right走到第三种水果位置,kinds>2了,此时我们会让left往前走,然后right回溯,在重新往前走。这就是我们的想法。
接下来我们看看有没有优化的可能。
这里我们就不在以特例来进行讲解,因为特殊例子并不具有代表性,可能你的想法值适用于这一个例子。如果随便拿一个数组都能满足,才是正确解法!
当right此时在往前走一个位置,kind=3,就不满足情况,因此我们现在让left往前走一步。现在我们讨论有没有必要让right回溯。left往前走只有两种情况,
一 :kinds 减小 right往前走
二 :kinds 不变 right不变
因此right根本不用回溯,要么不变,要么往前走。
这不就是同向双指针!这不是就是滑动窗口的思想吗!
上面的分析尤为重要,滑动窗口就那几步代码很简单,但是难的是你想到使用滑动窗口,这个why?很重要!
解法二:滑动窗口+哈希表
- left=0,right=0
- 进窗口
- 判断
出窗口 - 更新结果
当你知道用滑动窗口了,就可以使用特例来看看怎么写代码。
当出窗口一定要注意,因为进入这个hash的这种类型的树可不止一颗,如果我们用的是这种hash只统计类型,那出窗口减去这种类型树的时候时候,明明数组里面还有这种类型的树,但是一下就减没了,是有问题的,因此hash里面不光有这种类型,还要有这里类型树的个数。
进窗口然后判断,当判断后窗口里面一定是符号条件的,然后再更新结果。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int,int> mp;
int len=0;
for(int left=0,right=0;right<fruits.size();++right)
{
mp[fruits[right]]++;//进窗口
while(mp.size()>2)//判断
{
//出窗口
mp[fruits[left]]--;
if (mp[fruits[left]] == 0)
mp.erase(fruits[left]);
++left;
}
//更新结果
len=max(len,right-left+1);
}
return len;
}
};
虽然代码可以通过,时间有点久,这是因为我们使用了容器导致的,对于数据范围有限,可以不使用容器,这样会提高时间效率。
class Solution {
public:
int totalFruit(vector<int>& fruits) {
//unordered_map<int,int> mp;
int hash[100001] = {0};
int len=0,kinds=0;
for(int left=0,right=0;right<fruits.size();++right)
{
if(hash[fruits[right]] == 0)
kinds++;
//进窗口
hash[fruits[right]]++;
//判断
while(kinds>2)
{
//出窗口
hash[fruits[left]]--;
if (hash[fruits[left]] == 0)
--kinds;
++left;
}
//更新结果
len=max(len,right-left+1);
}
return len;
}
};
2.找到字符串中所有字母异位词
题目链接:438. 找到字符串中所有字母异位词
题目分析:
异位词:相同字母重新排序形成的字符串
在解决这道题前面,我们首先想如何快速判断两个字符串是否是 “异位词”
首先肯定会想把两个字符串排序一下,然后再每个字依次对比。这样是肯定能解决问题的,时间复杂度O(n+nlog^n)
还有其他更好的方法吗?
以前说过对于找重复的元素,什么最快?
肯定是哈希表。
因此我们直接用哈希表就可以了。
算法原理:
解法一:暴力求解+哈希表
我们可以把所有字符串中所有字串都找到结合哈希表,肯定能得到正确结果,但是肯定通过不了,时间复杂度太高了。
接下来想想有没有优化的可能
定义left,right指针,当right走到>len的位置,就停下来,因为超过p的长度了,根本就不可能是异位词了。然后left往前走一步,注意每次left只用走一步就可以了。因为此时left到right区间长度刚好等于len,所有left每次就走一步。
然后right也不用回溯,因为元素就已经在hash1中。并且right每次也就往后走一步就行了。走多了就不满足了。
到这里我们肯定直到是要用滑动窗口了,但是这里的滑动窗口和之前的不一样。
滑动窗口的分类:
1.非固定大小滑动窗口
利用单调性,规律,每次进窗口出窗口可能会好多次。判断出窗口是一个循环。
2.固定大小滑动窗口
满足特定条件,每次进窗口出窗口一次。判断出窗口就执行一次。
解法二:滑动窗口+哈希表
class Solution {
public:
bool check(int* hs,int* hp)
{
for(int i=0;i<26;++i)
{
if(hs[i] != hp[i])
return false;
}
return true;
}
vector<int> findAnagrams(string s, string p)
{
vector<int> ret;
int hashs[26]={0},hashp[26]={0};
for(auto& ch:p)
hashp[ch-'a']++;
int len=p.size();
for(int left=0,right=0;right<s.size();++right)
{
hashs[s[right]-'a']++;
if(right-left+1>len)
{
hashs[s[left]-'a']--;
++left;
}
if(check(hashs,hashp))
ret.push_back(left);
}
return ret;
}
}
这道题给的是小写字母,因此hash大小设为26。然后每次判断后都要check内部循环检查一下两个哈希表是否相等,因此这个时间复杂度是O(26*N)最终是O(N),对于这道题来说没问题,但是给你换成字符串就有问题了。
所以在优化一下:
优化:更新结果的判断条件
利用变量 count 来统计窗口中 “有效字符” 的个数
目前p中字符共3个,其中字符a、b、c分别占一个。那如果保证滑动窗口内有效字符(a、b、c)个数等于len,并且与p中字符a、b、c个数一一对应。那就能直接说明该滑动窗口是满足条件的,此时只要把left下标添加道返回结果就行了。这样就不用在每次循环判断两个hash表了。
当right第一次指向c,count+1,但是当right往后走再次指向c,就不要跟新count了,因为p中c字符就才1个!
当right到b的时候,count+1。此时判断一下,有效字符count个数为2,len=3,不相等。如果count个数正好等于3说明窗口里面全都是有效字符。
所以我们可以做到一边在滑动窗口里面添加元素,一边维护count,同时我们还可以知道什么时候是字母异位词。
然后right再走遇到a,a变成1,然后更新count+1。但是此时这个窗口大小已经超过len,因此先要出窗口left往后移一步。滑动窗口内字符c个数要减1,但是删掉之前count需不需要更新呢?并不需要,因为c=2,比p中c=1多一个字符,那删掉的c就是多余字符,因此并不需要更新count。在判断一下此时有效字符count==len,说明窗口里是合法字符,此时把left添加到返回数组就可以了。
总结:
进窗口: 进窗口后,hash2[in] <= hash1[in] —> count++
出窗口: 出窗口前,has2[out] <= hash1[out] —> count–
更新结果:count==len
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> ret;
int hash1[26]={0};//统计字符串 p 中每个字符出现的个数
for(auto& ch:p) hash1[ch-'a']++;
int hash2[26]={0};//统计窗口里面的每一个字符出现的个数
int len=p.size(),count=0;
for(int left=0,right=0;right<s.size();++right)
{
char in=s[right];
//进窗口 + 维护 count
if(++hash2[in -'a'] <= hash1[in -'a'])
count++;
if(right-left+1>len)//判断
{
//出窗口 + 维护 count
char out=s[left++];
if(hash2[out -'a']-- <= hash1[out-'a'])
count--;
}
if(count == len)
ret.push_back(left);
}
return ret;
}
}
3.串联所有单词的子串
题目链接:30. 串联所有单词的子串
题目分析:
注意这道题很重要一点就是 words 中所有字符串 长度相同。
其次这道题的意思是让在s字符串中,找到words数组中字符串的任意组合。找到后返回下标。
算法原理:
其实这道题和上面的 438. 找到字符串中所有字母异位词 ,解法思路一模一样,为什么这样说呢。因为上面这道题让找的是字母,这道题增加难道让找的是字符串的异位串!
words 中所有字符串 长度相同,我们设它为len长,然后把s中len个字符组合在一起,这不就是438. 找到字符串中所有字母异位词的题吗!
解法一:暴力求解+哈希表
不过有三处地方和上面不一样,需要注意,其他方面都是一样的解题思路
- 因为这里是字符串,因此我们使用unordered_map<string,int>容器,string用来记录字符串,int用来记录每个字符串出现的频次
- left 与 right 指针的移动
left 和 right每次移动的步长,是单词的长度 ----> len
- 因为我们指针是跳着走的,我们有的情况没有找完,因此滑动窗口执行的次数要len次
其他思路是完全一模一样的。
class Solution {
public:
vector<int> findSubstring(string s, vector<string> words) {
vector<int> ret;
unordered_map<string,int> hash1;//保存 words 里面所有字符串频次
for (auto& str : words) hash1[str]++;
int len=words[0].size(),m=words.size();
for(int i=0;i<len;++i)//执行len次滑动窗口
{
unordered_map<string,int> hash2;//维护窗口内字符串
//小心right往后走截取字符串越界问题!
for(int left=i,right=i,count=0;right+len<=s.size();right+=len)
{
// 进窗口 + 维护 count
string in=s.substr(right,len);
hash2[in]++;
if(hash1.count(in) && hash2[in] <= hash1[in])
++count;
//判断
if(right-left+1>len*m)
{
//出窗口 + 维护 count
string out=s.substr(left,len);
if(hash1.count(out) && hash2[out] <= hash1[out])
--count;
hash2[out]--;
left+=len;
}
//更新结果
if(count == m)
ret.push_back(left);
}
}
return ret;
4.最小覆盖子串
题目链接:76. 最小覆盖子串
题目分析:
本题是让在一个字符串找到包含另一个字符串的最短字串,其中我们看它的注意提醒,对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。这句话有点抽象,我们画图说明一下
注意看这个画圈的地方,这也是符合条件的字串,虽然B有两个字符。也就是说在s中找到符合要求的子字符串中对应字符可以大于的t字符串中的字符,这种字符串是满足条件的!
算法原理:
我们首先会想到暴力枚举把所有符合条件的子字符串找出来,一定可以找到其中最小的。那怎么找到符合条件的子字符串呢?我们这里还是借助哈希表。
解法一:暴力枚举+哈希表
下面看看有没有优化的可能,我们优化时暂时不用特例,搞一个任何情况下都满足的情况,现在left和right区间就是满足条件的最短子字符串。暴力枚举就是left+1,right回溯,然后重新往后找。
现在看看right有没有必要回溯在重新找。当left+1,可能有两种情况:
1.字符串不符合要求了
2.字符串依然符合要求
字符串不符合要求了那就是变短了,right如果回溯然后重新走到老位置,不还是不符合要求吗?此时right不需要回溯,右移往后走就行了
字符串依然符合要求,那right回溯之后还是走到老位置不还是符合要求吗?此时right不动就行了。
两个指针,同向! -----> 滑动窗口!
解法二:滑动窗口+哈希表
滑动窗口就那几步,我们这时可以使用特例来确实什么时候进窗口,判断,出窗口,更新结果
每次判断都要遍历数组,太麻烦了。我们把判断优化一下。
优化: 判断条件
使用变量 count 标记有效字符的种类
如果还像之前统计有效字符个数就有问题了,虽然有效字符个数相等但是并不是字串!
优化:
一定是字符个数相等才能说是种类一样!
进窗口后当 hash2[in] == hash1[in] count++
出窗口前当 hash2[out] == hash1[out] count- -
判断 count == hash1.size() 滑动窗口内字符种类和hash表中字符种类相等
class Solution {
public:
string minWindow(string s, string t) {
int hash1[128]={0};// 统计字符串 t 中每一个字符出现的频次
int kinds=0;// 统计有效字符有多少种
for(auto& ch : t)
{
if(hash1[ch]++ == 0)
++kinds;
}
int hash2[128]={0};// 统计窗口内每个字符的频次
int begin=-1,minlen=INT_MAX;
for(int left=0,right=0,count=0;right<s.size();++right)
{
//进窗口 + 维护count
char in=s[right];
if(++hash2[in] == hash1[in])
++count;
//判断
while(count == kinds)
{
//更新结果
if(right-left+1 < minlen)
{
minlen=right-left+1;
begin=left;
}
//出窗口 + 维护 count
char out=s[left++];
if(hash2[out]-- == hash1[out])
--count;
}
}
if(begin == -1) return "";
else return s.substr(begin,minlen);
}
};