目录
1.解析:给我们一个数组nums,要我们找出最小子数组的和==target,首先想到的就是暴力解法
出窗口: while(hash[s[right]]>1) hash[s[left++]]--;
出窗口:while(_k>k) if(nums[left++]==0) _k--;
更新len:len=max(len,right-left+1);
出窗口:while(ret>target) ret-=nums[left++]
更新len: if(ret==target) len=max(len,right-left+1);
进窗口:if(++hash2[s[right]]<=hash1[s[right]]) count++;
//分为两步,先是hash2[s[right]]++,然后判断是不是小于等于hash1[s[right]],是就说明,是一个有效字符
出窗口: if(right-left+1>n&&hash2[s[left]]--<=hash1[s[left++]]) count--;
判断减掉的这个 hash2[s[left]]--,是不是小于等于hash1[s[right]],是就说明,是一个有效字符
更新left: if(count==n) ret.push_back(left);
更新left:if(count == m) ret.push_back(left);
进窗口: if (hash1.count(s[right]) && ++hash2[s[right]] == hash1[s[right]]) count++;
更新len:if(len>n) return ""; return s.substr(_left, len);
滑动窗口,引入:
顾名思义,就是在同向双指针的条件下创建的一个窗口大小,让两个指针left 和 right 从左向右移动。
滑动窗口,本质:就是同向双指针;
当两个指针都可以做到不回退,都能同向进行的时候,就可以采用滑动窗口
怎么用,定义left=0,right=0,来进窗口,right++;出窗口的时候更新left++;知道right==n时结束
对于滑动窗口的效率提升,是利用数组的单调性,就比如要枚举left-right之间数字相加之和==target,那么sum+=nums[right] 到某个值后>target了,就不用继续加后面的值了,这样就不用枚举后面所有的情况,避免了很多没有必要的枚举
时间复杂度:代码可能是两层嵌套循环,但是实际上就只是right指针和left指针从左走到右,不回头的操作,那么就是最大时间复杂度也只是n+n次=2n 是O(N)级别
1.⻓度最⼩的⼦数组(medium)
题目的意思就是找出最短子数组的和==target ,这是最经典的滑动窗口的入门问题了,可以现在就动手解决一下,如果没明白再看解析。
1.解析:给我们一个数组nums,要我们找出最小子数组的和==target,首先想到的就是暴力解法
1)暴力:
就是从第一个元素开始进行相加,遍历后面的每一个元素,直到right移动到n-1的位置,left++,right回头重新开始遍历相加,然后遇到和sum==target的时候就更新len,最后返回最短的len,但是这种做法想都不用想,时间复杂度是O(N^2) 绝对会超时
2)优化,滑动窗口:
再讲滑动窗口做法前,我们要考虑为什么可以采用滑动窗口,为什么选择滑动窗口。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n=nums.size();
int len=INT_MAX,sum=0;
for(int left=0,right=0;right<n;right++)
{
//进窗口
sum+=nums[right];
//出窗口
while(sum>=target)
{
//为什么要先更新len,因为如果进来的条件是sum==target 那么就是要先更新最小值,最后在left++
len=min(len,right-left+1);
sum-=nums[left++];
}
}
return len==INT_MAX?0:len;
}
};
通过上面,就已经可以分析出,“滑动窗口”系列就是:
1.进窗口
2.出窗口
3.更新值
2.⽆重复字符的最⻓⼦串(medium)
如果是按照leetcode顺序刷的话,有多少人都死在了这题上面,其实如果了解了“双指针”->"滑动窗口"的过度,能很简单解决这题!!!
解析:
1)仍然是暴力解法:
不含有重复字符,那么就考虑到从第一个元素开始遍历,存到一个字符串str里,如果遇到了重复字符,就回退right,让left++,在从头开始遍历,当然这样的O(N^2) 绝对会超时;
2)优化:
进窗口:hash[s[right]]++;
出窗口: while(hash[s[right]]>1) hash[s[left++]]--;
更新len: right-left+1;
class Solution {
public:
int lengthOfLongestSubstring(string s) {
if(s.size()==1) return 1;
unordered_map<char,int> hash;
int n=s.size();
int len=0;
for(int left=0,right=0;right<n;right++)
{
//进窗口
hash[s[right]]++;
//出窗口
while(hash[s[right]]>1)
hash[s[left++]]--;
len=max(len,right-left+1);
}
return len;
}
};
总结:滑动窗口题目模板都大差不差,这题因为是考虑去掉重复字符,那么可以在每次循环结束都来判断最大的len,也不用在while里判断,因为可能回造成s字符串没有重复的字符,那么就进不去while
3.最⼤连续1的个数III(medium)
只要不对数组直接进行调整,移动什么的,其实都是比较简单的~ 因为下标都是固定的
解析:
1)暴力:
我在这题也卡了很久,暴力无非就是从第一个元素开始,遇到1,right++,遇到0,k--,直到k=0,然后更新len=right - left + 1;然后left++,right回头 重新遍历,重新跟新left,那么绝对超时O(N^2);
2)优化:
仍然是:
进窗口:if(nums[right]==0) _k++;
出窗口:while(_k>k) if(nums[left++]==0) _k--;
更新len:len=max(len,right-left+1);
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int len=0;
for(int left=0,right=0,_k=0;right<nums.size();right++)
{
if(nums[right]==0) _k++;
while(_k>k) if(nums[left++]==0) _k--;
len=max(len,right-left+1);
}
return len;
}
};
总结:
滑动窗口,要从暴力推导到优化还是比较好想的,滑动窗口,是保证两个指针left跟right依次往前运动,不后退,保证了效率的提升,不用产生无用的操作。
这题就是1.先要保证进窗口,//进窗口,一定要在前面
if(nums[right]==0) _k++;
这个就是进窗口的条件,当nums[right]==0 _k++;
2.那么现在考虑出窗口,while(_k>k) 在这个循环条件里面进行出窗口,其实仔细一些,如果在_k==k 时出窗口是不完美的,你还要保证nums[right+1]这个时候也是等于0 的。所以条件过于复杂苛刻,不便于实现,那么在while里面进行出窗口实际上就是让left++ 只是多加了一个if在满足nums[left++]==0 的条件下,就_k-- 来实现出窗口,这样就又可以让right继续往后移动!!!
4.将x减到0的最⼩操作数(medium)
这里面藏着一个小demo ,如果想不上去,这题简直无脑到直逼困难题!!!没有人会一直写代码,列举所有左右的情况,然后去相减吧,题目的意思就有点牵着别人走了。
解析:
1)暴力:
暴力属实无脑,开始先减减左边,然后减减右边,可以用dfs+回溯来解决,但是压栈太多,题目数据大,肯定会爆,直到x被减为0,那么就返回被更新的次数,如果只是单纯的这么写,那简直不敢想代码量。
2)优化:小demo
仍然是:
进窗口:ret+=nums[right]
出窗口:while(ret>target) ret-=nums[left++]
更新len: if(ret==target) len=max(len,right-left+1);
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum=0;
for(auto e : nums) sum+=e;
sum-=x;
if(sum<0) return -1;
int len=-1,ret=0;
for(int left=0,right=0;right<nums.size();right++)
{
//进窗口
ret+=nums[right];
//出窗口
while(ret>sum) ret-=nums[left++];
//更新len
if(ret==sum) len=max(len,right-left+1);
}
return len==-1?-1:nums.size()-len;
}
};
总结:
5.⽔果成篮(medium)
这题相对来说,还是比较简单的,有了上面几题的经验之后,这题只需要记录水果的种类个数不超过2,就可以一直装水果,然后记录最长子数组个数。
题目意思就是最多只能装两个不同种类的水果,求最大子数组的长度。
解析:
1)暴力:
那么就设置两个指针,left和right,从头开始遍历,设置一个计数器k,直到right遍历到第三个种类的水果时,停止,这个时候跟新len 的长度,在让left++,right回头重新进行遍历,那么绝对会超时,O(N^2);
2)优化:
仍然是:
进窗口:if (hash[fruits[right]]++ == 0) k++; 只要遇到 没遇见种类的水果时,k才++
出窗口:while (left < n && k > 2) if (--hash[fruits[left++]] == 0) k--; 当这种水果数量变成0了,也就说明这种水果不存在了,种类k-1
更新len:len = max(len, right - left + 1);
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int len = 0;
int n = fruits.size(), k = 0;
unordered_map<int,int> hash;
for (int left = 0, right = 0, tmp = 0; right < n; right++)
{
//进窗口
if (hash[fruits[right]]++ == 0) k++;
//出窗口
while (left < n && k > 2)
if (--hash[fruits[left++]] == 0) k--;
//更新len
len = max(len, right - left + 1);
}
return len;
}
};
总结:
6.找到字符串中所有字⺟异位词(medium)
题目意思就是在s里找p,p的顺序是任意的,但是要包含所有字符,返回s里面所有满足条件的字符下标。没有思路的话,可以先试试暴力枚举:
解析:
1).暴力:
暴力枚举其实也有难度,比如要先讲p排序,不然要列举出p的各种顺序肯,太复杂,对比不过来,然后设置str+=s[right],长度len=right-left+1 == p.size(); 然后对str进行排序,在判断str==p? 如果等于就存入str的下标,否则就跳过,left和right都++;
2).小优化:
用两个hash表来比较纯如的字符是否完全相等,如果是,就加入left;但是这样会有一个用例超时,说明还可以继续优化。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int n=s.size(),m=p.size();
unordered_map<char,int> hash1;for(auto e : p) hash1[e]++;
unordered_map<char,int> hash2;
vector<int> v;
for(int left=0,right=0;right<n;right++)
{
hash2[s[right]]++;
while(right-left+1>m) hash2[s[left++]]--;
//判断两个字符数组是否相等
if(hash1.count(s[right]))
{
int f=0;
for(int i=0;i<m;i++)
{
if(hash2[p[i]]!=hash1[p[i]])
{
f=1;
break;
}
}
if(f==0) v.push_back(left);
}
}
return v;
}
};
大优化:
进窗口:if(++hash2[s[right]]<=hash1[s[right]]) count++;
//分为两步,先是hash2[s[right]]++,然后判断是不是小于等于hash1[s[right]],是就说明,是一个有效字符
出窗口: if(right-left+1>n&&hash2[s[left]]--<=hash1[s[left++]]) count--;
判断减掉的这个 hash2[s[left]]--,是不是小于等于hash1[s[right]],是就说明,是一个有效字符
更新left: if(count==n) ret.push_back(left);
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int m=s.size(),n=p.size();
unordered_map<char,int> hash1; for(auto e : p) hash1[e]++;
unordered_map<char,int> hash2;
int count=0;
vector<int> ret;
for(int left=0,right=0;right<m;right++)
{
//进窗口
if(++hash2[s[right]]<=hash1[s[right]]) count++;
//出窗口
if(right-left+1>n&&hash2[s[left]]--<=hash1[s[left++]]) count--;
//更新left
if(count==n) ret.push_back(left);
}
return ret;
}
};
总结:
7.串联所有单词的⼦串(hard)
这一题,简直就是跟上一题找异位词,百分之99都一样,所以一定要弄懂上一题。
题目意思任然是要在words中的每个单词都以任意顺序组合,然后再s里面找到这种组合的字串,返回所有满足条件的下标。
解析:
1).暴力
就是设置两个指针,left 和 right 然后往后判断,left 到 right 这个区间的字符串是不是能够等于words组合成的顺序,显然,这是绝对不可取的,要进行优化。
2).优化:
小demo,看到这种任意组合的字符串,我们不可能无序的组合,然后在s里面找字串去进行对比,这肯定不可取,那么我们就要跟上题大优化一样,设置计数器count 来记录有效字符串的个数,这样就可以很方便的进行记录。
进窗口:string str = s.substr(right, len);
//进窗口
hash2[str]++;
if(hash1.count(str) && hash2[str] <= hash1[str]) count++; //记录有效字符串的个数
出窗口:if(right - left + len > m * len)
{
string _str = s.substr(left, len);
if(hash1.count(_str) && hash2[_str] <= hash1[_str]) count--;
hash2[_str]--;
left += len;
}
更新left:if(count == m) ret.push_back(left);
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
int n = s.size(), m = words.size(), len = words[0].size();
unordered_map<string, int> hash1; for(auto& e : words) hash1[e]++;
vector<int> ret;
for(int i = 0; i < len; i++)
{
unordered_map<string, int> hash2;
for(int left = i, right = i, count = 0; right + len <= n; right += len)
{
string str = s.substr(right, len);
//进窗口
hash2[str]++;
if(hash1.count(str) && hash2[str] <= hash1[str]) count++;
//出窗口 固定的滑动窗口
if(right - left + len > m * len)
{
string _str = s.substr(left, len);
if(hash1.count(_str) && hash2[_str] <= hash1[_str]) count--;
hash2[_str]--;
left += len;
}
if(count == m) ret.push_back(left);
}
}
return ret;
}
};
总结:
8.最⼩覆盖⼦串(hard)
经过上面那么多题的磨练,这题是我为数不多的自信题,调试了两个小时第一次自己解决一道困难题,其实相对来说还是比较简单的。
题目意思就是找到最小字串,要涵盖t中的全部字符,可以重复包含。
解析:
1).暴力:
任何优化都是通过暴力推导的,所以暴力真的很重要。
暴力就是无脑遍历s,找到字符串长度要大于等于t.size() 然后进行比较,如果包含全部t的字符,就记录长度len,直到求出最短长度。肯定会超时,而且暴力不好实现,所以还是要优化。
2).优化:
进窗口:
if (hash1.count(s[right]) && ++hash2[s[right]] == hash1[s[right]]) count++;
出窗口: while (len>m&&count == hash1.size())
{
if (len>m&&len > right - left + 1)
{
len = right - left + 1;
_left = left;
}
if (hash1.count(s[left]) && --hash2[s[left]] < hash1[s[left++]]) count--;
while (left < n && hash2[s[left]] == 0) left++;//跳过非种类字符
}
更新len:if(len>n) return "";
return s.substr(_left, len);
class Solution {
public:
string minWindow(string s, string t) {
int n = s.size(), m = t.size();
if (n < m) return "";
unordered_map<char, int> hash1; for (auto e : t) hash1[e]++;
unordered_map<char, int> hash2;
int _left = 0, len = INT_MAX;
int left = 0, right = 0;
while (right < n && hash1.count(s[right]) == 0) right++, left++;
if (left >= n || right >= n) return "";
int count = 0;//记录种类
for (right; right < n; right++)
{
//进窗口
if (hash1.count(s[right]) +hash2[s[right]] == hash1[s[right]]) count++;
//出窗口
while (len>m&= hash1.size())
{
if (len>m&&len > right - left + 1)
{
len = right - left + 1;
_left = left;
}
if (hash1.count(s[left]) && --hash2[s[left]] < hash1[s[left++]]) count--;
while (left < n && hash2[s[left]] == 0) left++;//跳过非种类字符
}
}
if(len>n) return "";
return s.substr(_left, len);
}
};
因子都是成对出现的,那么我们就只需要在for里面判断 x*x < n 那么就判断是否有一个m%x==0 那么当x>k 就满足,另一个因子就是 m / x > k 就也会满足。
总结一下,“滑动窗口”问题其实也就是模板问题,只要能明白“该问题是同向的双指针问题”,那么就可以往滑动窗口靠,就是“查找子数组”,就分三大点:进窗口_出窗口_更新len!!!
我觉得我自己的进步挺大的,希望对你也有帮助哦!~