好久没有写博客了,自从上半年蓝桥杯结束后,就有点懈怠了
最近两三周才又慢慢刷起题来,也顺便记录下自己的成长!
今天是滑动窗口的章节,前两周刷了字符串、双指针、模拟。这些板块我都在leetcode上找了些题,并且每个板块大概刷了两三天,其中包括了从前学习这些东西所做过的题,来复习一遍,和找新的题,以前的题有一半都做不上来了。。
蛮久不刷确实会掉题感啊,闲话少叙,开始今天的新内容
219.存在重复元素II
219. 存在重复元素 II - 力扣(LeetCode)https://leetcode.cn/problems/contains-duplicate-ii/?envType=list&envId=24zW97w8第一题,是个简单题,是昨天刷的,虽然是简单题,但是这两天刷滑动窗口给我的感觉是,滑动窗口虽然是双指针的变体,但是还是比想象中要难得多
这道题要求是找到两个数,这两个数相等的前提下,它们的下标值不能大于k
先是我自己写的代码:
cpp
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
int i=0,j=0;
if(nums.size()==1)return false;
unordered_set<int>hset(nums.size());
for(i=0;i<nums.size();++i){
if(j>0)hset.erase(nums[i-1]);
while(j<nums.size()&&!hset.count(nums[j])&&abs(j-i)<=k){
hset.insert(nums[j]);j++;
}
//跳出循环有两种可能,一种是找到相同字符,一种是超过k
if(j<nums.size()&&hset.count(nums[j])&&abs(j-i)<=k)return true;
if(j==nums.size())return false;
}
return false;
}
};
这段代码思路就是,不停的移动右窗口,直到右窗口到达边界或者找到了两个相等的数据,又或者当前窗口已经大于k了。那么我们就停下来,去判断是何种原因导致的停下循环,如果是在右边界没出界的前提下,并且找到了相等数据而且下标差还不大于k,那么直接返回true
如果右边界出界直接返回false,因为我们设置的是此时右窗口不会向前走只能向后走
如果既没有到达右边界,又没有相等数据,那只能说明出k范围了,这时我们直接删除当前窗口左元素,让左窗口右移即可!
滑动窗口的题有很多都要用到哈希,什么样的题要用滑动窗口呢?
要求字符串或者数组中的某一部分的数据,连续子串类型的
用两个指针去分割这个字串,所以形象的称为滑动窗口
什么时候搭配哈希?需要判断字符是否重复或者需要判断一个子串中不相邻的某两个元素之间的关系,用哈希存储窗口内的数据,以便查重
解题思路:
使用哈希表set来存储滑动窗口的数字,如果满足新数字和窗口里任何数字都不相等且在k范围内条件则加入set
否则判断如果在k内且有数字相等,则说明找到了正确答案
再看官方答案,官方答案也是用set,但是比我们简洁的多
cpp
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_set<int>set;
for(int i=0;i<nums.size();i++){
if(i>k)set.erase(nums[i-k-1]);
if(set.find(nums[i])!=set.end())return true;
set.emplace(nums[i]);
}
return false;
}
};
思路是相同的,不过它不用左右边界去记录窗口,只用i,相当于单指针,当i大于k说明此时每次都需要减少左窗口的值,用i-k-1即可,然后去查找,因为先判断的是否两数据相减超出k所以我们直接看是否能找得到,能找得到就直接回true就完事了,这种方法也是保证了窗口一直保持最大时候,去删除左数据。
再看卡尔哥的题解
cpp
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int,int>map;
for(int i=0;i<nums.size();i++){
if(map.find(nums[i])!=map.end()&&i-map[nums[i]]<=k)
return true;
map[nums[i]]=i;
}
return false;
}
};
用map是映射数据结构的优点,键存储当前数字,值存储当前下标,写法很直观,通俗易懂整体思路和前面的一样
643.子数组最大平均数I
643. 子数组最大平均数 I - 力扣(LeetCode)https://leetcode.cn/problems/maximum-average-subarray-i/?envType=list&envId=24zW97w8求的是在一个字符串里找到平均数最大且长度为k的子数组
也是先看自己写的思路:
cpp
class Solution {
public:
double findMaxAverage(vector<int>& nums, int k) {
double res=INT_MIN;int left=0;double sum=0;
for(int i=0;i<nums.size();i++){
if(i>k-1){sum-=nums[left++];}
sum+=nums[i];
if(i>=k-1)
res=max(res,sum/k);
}
return res;
}
};
用一个变量来记录每一次当前范围子数组的和,然后每次遍历都求一次平均数
判断当前窗口是否大于k,大于则减去左窗口的值,这里的判断就明显整洁了不少吧,这是上一道题吸收来的经验。
这里也可以写成if(i>=k)sum-=nums[i-k]这样就不需要有left来记录左边界了
i控制右窗口,left控制左窗口
到了最大窗口数sum每次会减少相应的值,在循环中不停变化平均值,最后返回平均值即可
需要注意的是不是每一次都要取平均值,只有当i走到合适的位置后
也就是说只有当窗口大小等于k时候才取
再来看看官方题解
cpp
class Solution {
public:
double findMaxAverage(vector<int>& nums, int k) {
int sum=0,maxsum=0;
for(int i=0;i<k;i++)sum+=nums[i];
maxsum=sum;
for(int i=k;i<nums.size();i++){
sum-=nums[i-k];sum+=nums[i];
maxsum=max(sum,maxsum);
}
return 1.0*maxsum/k;
}
};
思路也是滑动窗口
但是时间明显少了一些
这可能是因为它只求了一次平均值
做法就是,先循环,让窗口填满记录一下当前和
一开始先加数加到k,然后右窗口向前走一次,左窗口便向右走一次,
从窗口下一个位置遍历,sum加上新数,并且减去旧窗口数(最左边的)
使用i-k,这一点十分巧妙
然后最后答案返回遍历过程中k个连续子串的最大和
取平均数
1763. 最长的美好子字符串
1763. 最长的美好子字符串 - 力扣(LeetCode)https://leetcode.cn/problems/longest-nice-substring/?envType=list&envId=24zW97w8在给定字符串中寻找子串,子串满足若出现某个字母,则该子串中必须含有其对应的大写/小写字母,数量不限,比如说Aaa这也是可行的
一开始我的思路是用哈希表存窗口的字母,并且把它变成对应的大小写字母,如果再看到则删掉哈希存储的字符,然后判断当前窗口的哈希表是否存在数据,如果存在则说明该字串有一些不成对的字母,若没有可以比较是否为最长,然后更新结果值
后来才觉得,想简单了,因为Aaa也是可行的
然后就想不出来其他解法了看了题解
cpp
class Solution {
public:
string longestNiceSubstring(string s) {
if(s.size()<2)return "";string res;
for(int i=2;i<=s.size();i++){
for(int j=0;j+i-1<s.size();j++){
string str=s.substr(j,i);
if(str.size()>res.size()&&fun(str))res=str;
}
}
return res;
}
bool fun(string&s){
int AA[26]={0};int aa[26]={0};
for(char c:s){
if(c-'A'<=26)AA[c-'A']++;
else aa[c-'a']++;
}
for(int i=0;i<26;i++){
if(aa[i]>0&&AA[i]==0||aa[i]==0&&AA[i]>0)return false;
}
return true;
}
};
题解的这种方法很好也容易理解
它是外层循环规定此时最大窗口为多大,然后内层循环从字符串第一个字符往后走,依次增大,窗口也随j移动
内层循环判断如果此时为完美子串,且当前字串大,那么给res
如何判断完美子串?
函数使用两个数组充当哈希表,一个存大写字母,一个存小写字母
将当前j位置的窗口子串截取下来,通过遍历向两数组填数
遇到对应字符相应位置自增
然后填数完毕,判断如果对应位置大小写字母数组都大于0则说明是完美字串
这里我的理解是,如果不写自增,只是用等于1来标记大/小字母出现过,也是可以的
而且那个双for循环,假如写成i控制字符串起始位置
然后j控制子串结束位置,也相当于在字符串各个位置使用不同窗口进行截取
效果应该是一样的
重要的不是这点,重要的是如何判断完美字串
使用两个数组记录,对应位置是否出现的思想非常重要!
遇到不能用一个哈希表解决的滑动窗口问题时,可以思考能不能用两个哈希表解决它们
395.至少有k个重复字符的最长子串
395. 至少有 K 个重复字符的最长子串 - 力扣(LeetCode)https://leetcode.cn/problems/longest-substring-with-at-least-k-repeating-characters/?envType=list&envId=24zW97w8个人认为很难想思路,官方题解我看不懂,吐槽一下官方题解通常都有点晦涩难懂
所以我参考了其他用户高手
我自己当然也有尝试解题,思路是有的,也实现了,但是就是超时
class Solution {
public:
int longestSubstring(string s, int k) {
int res=0;
for(int i=0;i<s.size();i++){
for(int j=i;j<s.size();j++){
if(fun(s,i,j,k)&&res<j-i+1)res=j-i+1;
}
}
return res;
}
bool fun(string &s,int left,int right,int k){
int arr[26]={0};
for(int i=left;i<=right;i++)arr[s[i]-'a']++;
for(int i=0;i<26;i++){
if(arr[i]!=0&&arr[i]<k)return false;
}
return true;
}
};
我对这个代码做了很多优化,比如把哈希表从map改成数组,这样可以免去insert的时间消耗,
把使用substr截取字符串改成传下标,个人认为代码还是十分简洁的,不过双for循环就是容易超时,没有办法。。。
class Solution {
public:
int longestSubstring(string s, int k) {
int res=0;
for(int i=0;i<s.size();i++){
for(int j=i;j<s.size();j++){
if(fun(s,i,j,k)&&res<j-i+1)res=j-i+1;
}
}
return res;
}
bool fun(string &s,int left,int right,int k){
int arr[26]={0};
for(int i=left;i<=right;i++){
if(arr[s[i]-'a']>=k)continue;
arr[s[i]-'a']++;
}
for(int i=0;i<26;i++){
if(arr[i]!=0&&arr[i]<k)return false;
}
return true;
}
};
这是人家写的代码,时间复杂度为ON
循环是以26个不同的字母为限制的,即每次只允许窗口内存在n个不同的字母
设置变量来分别存放当前窗口内不同种类字符的个数、符合该种字符个数等于k的字符有多少个、窗口左边界、窗口右边界
变量设置好后,开始进入第一个while,它是扩张右窗口的
如果当前数组位置为1,diff++,说明有不同的字符第一次加进来
如果当前位置数值为k则count++说明当前字符出现次数第一次达到k,注意这里并不是大于等于k时候count++,这样的话后续加进来该字符会导致重复计数
如果当前窗口出现的字符种类大于i,进入左边界缩小的阶段,进入的是循环而不是if判断语句
循环中如果左边界在右边界左侧,说明该窗口有缩减的必要,并且还要保证此时窗口字符种类大于i
进入之后是和上面类似的判断,不停增大left直到字符种类在允许的范围内
值得注意的有两点:
第一:判断窗口种类个数是否超过i的循环,应该在扩大右边界循环内部,每扩大一次右边界,并完成相应变量增加后,就立即判断此时左边界是否应该缩小
第二:判断完左右边界之后,立即进行的是一句判断
如果当前所允许的字符种类个数等于当前窗口的字符种类个数,并且窗口里任意字符的个数都大于或等于k,则判断是否应该更新答案值
为什么要判断现在最大允许的字符种类个数呢?
因为该循环是由1开始到26结束,每次判断的是最大允许范围这能保证当前窗口前提下,找到的是最大的数值,这样才可能需要更新数据
一定要注意不要把更新数据这条语句不小心写到循环外面了,这样如果在窗口移动时有新数据,就添加不上了
以上便是今天滑动窗口练习的全部内容,感觉有帮助的小伙伴们可以三联支持下,现在正是需要涨粉的时候2333
如果大家想看其他专栏的,比如动态规划、哈希表、贪心算法或是其他数据结构可以看往期文章
最近打算更一些更偏重基础的内容,以巩固基础,大家可以多多讨论,或者有想看的题解(当然不要太难)也可以评论发出来大家讨论,尽量做到日更吧