什么是窗口?就是符合题目要求的区域内的数据,将每次符合数据的窗口内的数据记录下来,然后将窗口后移,寻找其他符合要求的数据,每次进入窗口和退出窗口都需要一定的要求
一、长度最小的子数组
LCR 008. 长度最小的子数组 - 力扣(LeetCode)
思路
代码
class Solution {
public:
bool istarget(int x, int target)
{
if (x >= target)
return true;
return false;
}
int minSubArrayLen(int target, vector<int>& nums) {
//先判断是否有总和是否大于target,如果不是那么就直接返回0
int judgesum = 0;
for (auto e : nums)
{
judgesum += e;
}
if (judgesum < target)
return 0;
int len = nums.size(), left = 0, right = 0;
//开拓一个数组,放入符合条件的长度,之后进行遍历,进行排序,然后返回最小值
vector<int> ans;
int sum = 0;
while (left < len && right < len)
{
sum += nums[right];
if (istarget(sum,target))//如果超过了
{
while (sum>=target)
{
ans.push_back(right - left + 1);
sum -= nums[left];
++left;
}
++right;
}
else//不满足,那么右指针往后走
{
++right;
}
}
sort(ans.begin(), ans.end());
return ans[0];
}
};
二、无重复字符的最长子串
LCR 016. 无重复字符的最长子串 - 力扣(LeetCode)
思路
照常还是使用滑动窗口,符合要求的就进入窗口并且记录,不符合要求的就出窗口
这道题的重点就是在于如何判断已经出现过的字母——需要用到哈希表
但是这里会有特殊情况
若是在中间地方出现了重复的,例如pwwke,那么我们前边的pw这两个字母都不要了,重新统计
代码
int lengthOfLongestSubstring(string s) {
unordered_map<char,int> mp;
int l=0,r=0;
int ans=0;
while(r<s.size())
{
mp[s[r]]++;//右指针对应的下标的字母个数往上加
while(mp[s[r]]==2)//如果对应的字母出现两次了,说明这时候就要更新开头的位置,并且要将其对应的字母的个数要重置,避免多余的删除
{
mp[s[l++]]--;
}
ans=max(ans,r-l+1);//每一次都要更新最长的长度
r++;//右指针往后
}
return ans;
}
三、最大连续1的个数Ⅲ
1004. 最大连续1的个数 III - 力扣(LeetCode)
思路
这个题目其实可以暴力枚举,但是枚举终究是会超时的,那我们需要用什么来减少时间复杂度?
——滑动窗口
此处的重点是在于将统计翻转后连续1的个数——>一个连续数组里0的个数
然后,我们要注意
当0的个数=k的时候,不是立刻停止读取,而是要判断0下一个数字是否为0,若为0,那就停止right的移动。
若为1,那么right往后的话,连续数组里0的个数还是没有超过k,也就是翻转的次数没有超过k。
为了方便,我选择了当统计0的个数>3的时候,也就是读取到多余的一个0的时候,就开始移动窗口。
并且此题与上两题不同的是,这题是实时记录长度,其他两题是进入窗口后才进行数据的记录
代码
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int ret=0;
for(int left=0,right=0,zero=0;right<nums.size();right++)
{
if(nums[right]==0)zero++;
while(zero>k)
{
if(nums[left++]==0)
zero--;
}
ret=max(ret,right-left+1);
}
return ret;
}
};
四、将x减到0的最小操作数
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
思路
这道题还是按照滑动窗口,但是题目中给到“移除数组最左端或最右边的元素”。
左右遍历是可以的,但会使代码量及其复杂,我们是否可以换个思路——找到连续的数组使其符合什么关系?
所以我们的思路就转换为——找到连续的数组,使其总和为sum-x,并且找到其最长的长度
代码
int minOperations(vector<int>& nums, int x) {
int len=nums.size(),sum=0;
for(auto e:nums)
{
sum+=e;
}
int target=sum-x;
int left=0,right=0,maxlen=-1,testsum=0;
while(left<len&&right<len)
{
testsum+=nums[right];
if(testsum==target)
{
maxlen=max(maxlen,right-left+1);
++right;
}
if(testsum>target)
{
while(testsum>target&&left<len)
{
testsum-=nums[left++];
}
//去重后也需要进行一个判断
if(testsum==target)
{
maxlen=max(maxlen,right-left+1);
++right;
}
}
if(testsum<target)
{
++right;
}
}
if (maxlen == -1)//若还为-1,说明没有找到
return -1;
return (len-maxlen);//总长度减去连续的最长长度,便是两端最短的长度
}
五、找到字符串中所有字母异位词
LCR 015. 找到字符串中所有字母异位词 - 力扣(LeetCode)
思路
这道题就是用滑动窗口,依次读取——但是要进行无序的比较,如何比较?——用哈希表。
用哈希表先记录要对照的字符串每个字母出现的次数,然后读取到窗口数——p的大小
然后进行判断
暴力枚举的思路
这样子是大致思路,是暴力枚举和滑动窗口的结合,但是判断会遇到两种情况
1.字符串里交替出现要检索的p,例如s="abab",p="ab",那么每次判断后都要清除前一个已经存在的哈希表的数据,并且将left指针向右移动
2.字符串里有连续的字符,但是中途出现了一个不属于p中的字符,那么这时候就要归位,要从不属于p这个字符后边位置开始。例如s="cbaeabcd",p="abc"
3.字符串里有连续的相同字母出现,此时那么就需要去重。例如s="bbcaacb",p="abc"。
优化
选用一个count来获取不重复有效字符的个数
这个例子不能反映count的优化程度,再举个例子
代码
vector<int> findAnagrams(string s, string p) {
vector<int> ans;
int hashtest[26] = { 0 };
int hashsearch[26] = { 0 };
for (auto ch : p) hashtest[ch - 'a']++;//记录个数
int m = p.size();
for (int left = 0, right = 0, count = 0; right < s.size(); right++)
{
char in = s[right];
if (++hashsearch[in - 'a'] <= hashtest[in - 'a'])++count;
if (right - left + 1>m)//此时超出了窗口数,那么
{
char out = s[left++];
if (hashsearch[out - 'a']-- <= hashtest[out - 'a']) count --;
}
if (count == m) ans.push_back(left);
}
return ans;
}
六、串联所有单词的子串
思路
这道题和第五题中类似,只不过是从单词转变成为了字符串,所以我们只需要将判定条件变为,每经过一个子串的长度,那么就用哈希表进行判断,并且这个哈希表的键值对的键就是string类型。
同时,由于判断的长度总共为p中所有单词的长度,所以就是最多只能移动到words[0].size()*words.size()处
需要注意的是,我们需要用substr函数来读取字符串。
剩下的判定方式和有效个数的读取就是和第五题中单词的方式一样。
一旦满足的有效个数,那么就会进入去重的步骤。
代码
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string, int> hash1;
vector<int> ans;
for (auto& s : words)hash1[s]++;//将每一个字符串保存起来
//我靠,用字符串比较
int len = words[0].size(), m = words.size();
for (int i = 0; i < len; i++)//每一段len就记录长度
{
unordered_map<string, int> hash2;
for (int left = i, right=i, count = 0; right+len <= s.size(); right+=len)//count记录的是有效字符串的长度
{
string in = s.substr(right, len);
hash2[in]++;
if (hash2[in] <= hash1[in]) count++;//如果出现一次以下,count才会++
if (right - left + 1 > len*m)//说明此时符合全部字符串的长度
{
string out = s.substr(left, len);//当做一个整体表明是要出窗口的
if (hash2[out]-- <= hash1[out]) --count;
left += len;//那么此时就left往后一个字符的长度
}
if (count == m)//有效字符的个数等于字符个数,那么就往答案里边存放数据
{
ans.push_back(left);
}
}
}
return ans;
}
七、水果成篮
思路
1.双指针
题目的要求就是获取两种类型的最大长度,我们可以定义两个变量firsttype和secondtype,分别代表着第一个树的类型,第二个树的类型。当读取到第三种类型的树时,那么就记录下长度,然后进行去重,并且从一开始读取到的第二种树的类型开始进行重新的判断。
代码
int totalFruit(vector<int>& fruits) {
int len = fruits.size();
if(len==1)
return 1;
if(len==2)
return 2;
vector<int> ans;
int left = 0, right = 1;
int firsttype = fruits[0], secondtype = -1;//记录两个种类对应的数字,而且还可以用来存放不同种类出现的位置
int firsttmp = 0, secondtmp = 0,flag=0,transtime=0;
while (right < len)
{
//mp[fruits[right]]++;
if ((!flag)&&fruits[right] != firsttype)//开始判断第二次出现的种类
{
flag = 1;//表明出现了第二个数据
secondtmp = right;//记录位置
secondtype = fruits[right];//记录对应的数字
transtime++;
}
if (flag)//flag=1表明已经有两个种类了
{
if (fruits[right] != secondtype && fruits[right] != firsttype)//出现第三种类型
{
ans.push_back(right - left);//直接放入数据,并且更新位置
firsttype = secondtype;
firsttmp = secondtmp;//直接更新第二个种类的位置
left = firsttmp;
flag = 0;
right=firsttmp;
continue;
//这个判断的循环,right不能计入
}
if (right == len-1)//right已经到边界都没有出现第三个数字
{
ans.push_back(right - left+1);
++right;
continue;
}
}
++right;
}
if (transtime == 0)//没有第二类树了,那么就直接返回数组的长度
{
return fruits.size();
}
sort(ans.begin(), ans.end());
return ans[ans.size() - 1];
}
2.滑动窗口
要知道哈希表不仅能保存每种类型出现的个数,还能进行出现的类型的个数的统计。
所以我们可以通过统计hash表中的长度,来判断是否出现了第三种树,出现了第三种树后,那么就直接进行类型的去重,并且可以实时观察哪个种类优先被去重到0——也就是当前连续的树种类不多于2。
此处用for循环来判断,这样子方便右指针right的移动
代码
int totalFruit(vector<int>& fruits) {
unordered_map<int,int> hash;
int ret=0;
for(int left=0,right=0;right<fruits.size();right++)
{
hash[fruits[right]]++;
while(hash.size()>2)//表示种类大于2
{
hash[fruits[left]]--;
if(hash[fruits[left]]==0)
hash.erase(fruits[left]);
left++;
}
ret=max(ret,right-left+1);
}
return ret;
}
八、最小覆盖子串
LCR 017. 最小覆盖子串 - 力扣(LeetCode)
思路
本题思路跟第五题中找字母异位词类似,这不过第五题中要找的是连续的,此题找的不是连续的,所以我们仅需要一个哈希表来记录,并且同样用count来记录有效字符个数,当有效字符个数大于要查找的字符数时,那么就不进行有效字符个数的记录。
而且此处不需要对于多余的字母进行去重,仅需要对于符合有效字符个数的时候进行记录。
代码
string minWindow(string s, string t) {
unordered_map<char, int> hashtest,hashsearch;
for (auto& e : t)
{
hashtest[e]++;//记录下要查找的t的字符的种类
}
string ret = "";
int left = 0, right = 0, count = 0;
for (; right < s.size(); right++)
{
hashsearch[s[right]]++;
if (hashsearch[s[right]] <= hashtest[s[right]]) count++;//若超出这个字符限定的次数,那么就不会计入有效字符个数
while (hashsearch[s[left]] > hashtest[s[left]])//去重
{
--hashsearch[s[left++]];
}
if (count == t.size())
if (ret.empty() || right - left + 1 < ret.size()) ret = s.substr(left, right - left + 1);
}
return ret;
}
总结
滑动窗口这部分的难点主要是在于:进窗口条件+出窗口条件+进窗口时对于变量的处理+出窗口时对于变量的处理+对于数据的记录。
弄清楚这些条件后,便可以轻松解出题目。