目前已更新系列
当前--滑动窗口系列算法总结与题解一
由于总结的该类题目较多所以会氛围两期进行练习发布博客
思路与小结
- 使用滑动窗口的条件,或者说能用滑动窗口来解决什么问题?
-
- 1、窗口满足单调性:即当我扩展窗口,或缩小窗口是要么整体值单调递减、要么单调递增,比如窗口维持的是一个总值,那么扩张窗口那么这个值就一定增加,缩小窗口值那么该值就减少
- 2、我的习惯是扩展右边窗口,然后看条件所有窗口
- 3、一般窗口中会维护一个值,比如在无重复子串中,窗口中可以维持一个词频统计数组,比如在长度最小的子数组中,窗口中维持一个窗口中的总值
- 4、一般使用窗口条件是连续:比如子数组、子串等
接雨水
题目描述:
解析
方法一:两次遍历
思想:相当于动态规划即,先将记录当前位置i的左边最大值(可以包含i位置也可以不包含i位置)和右边的最大值(可包含i也可不包含i),然后可以将从前往后的找左边最大值和找答案放在同一次遍历中,积水位置就是找出左右两边最大值的较小的一个,然后看看这个值是否比当前位置i的位置高,如果高于那么积水
方法二:双指针-一趟完成
核心思想就是开辟两个数组一个记录左边最大值(比较时包含left指针位置的值)一个记录右边最大值(比较时包含rigth指针的最大值),然后找出左指针左边最大值lm和右指针位置的右边最大值rm,哪边低哪边就可能积水假设左边低那么就ans+=lm-heigth[left]然后left++;
这样是因为lm是包含了heigth[left]的left左边的最大值,从而得到left位置的积水情况
//方法二使用双指针进行一趟完成,核心就是,通过左右指针进行靠拢
//积水是,左指针位置和有右指针位置,那个矮那个位置就有机会积水,注意我们左右指针记录的最大值是包括当前位置i的
//由于这个是左右两边同时向中间靠拢,所以不能只用一个数组了,必须开两个数组来记录左右最大值
public int trap1(int[] height) {
int ans=0;
int[] lmax=new int[height.length];
int[] rmax=new int[height.length];
int left=0,rigth= height.length-1;
//初始化
//开始时最左最右不积水
lmax[left]=height[left];
rmax[rigth]=height[rigth];
left++;
rigth--;
while (left<=rigth){
//找当前位置左右最大值,最大值包含当前下标
lmax[left]=Math.max(lmax[left-1],height[left]);
rmax[rigth]=Math.max(rmax[rigth+1],height[rigth]);
if (lmax[left]<=rmax[rigth]){
ans+=lmax[left]-height[left];
left++;
}else {
ans+=rmax[rigth]-height[rigth];
rigth--;
}
}
return ans;
}
无重复字符串
题目描述
解析:滑动窗口
解析看代码
//思路:滑动窗口:窗口中维护一个词频统计表
//缩小窗口(左窗口往右移动的条件):当前尝试扩展的又窗口的值在维护的窗口内,left++直到将原来位置的该值移除窗口
//ans:该过程中的最大值长度
//思路:滑动窗口:窗口中维护一个词频统计表
//缩小窗口(左窗口往右移动的条件):当前尝试扩展的又窗口的值在维护的窗口内,left++直到将原来位置的该值移除窗口
//ans:该过程中的最大值长度
public int lengthOfLongestSubstring(String s) {
char[] arr = s.toCharArray();
int[] cnt=new int[255];
int ans=0;
//默认右窗口移动
for (int l = 0,r=0; l <=r&&r<s.length() ; r++) {
char index=arr[r];
while (cnt[index]!=0&&l<=r) cnt[arr[l++]]--;
cnt[index]++;
ans=Math.max(ans,r-l+1);
}
return ans;
}
优化
将词频统计数组用起来
精华:l=Math.max(l,cnt[index]+1);这个保证了左指针的值,不管当前扩展窗口位置在没在窗口中或者之前出现过没,如果没出现过那么值为-1,去max后还是左窗口值,如果出现过但是没在窗口中那么还是当前做窗口值,如果在窗口内,那么直接更新为窗口内该值的下一个坐标
class Solution {
//将窗口中维护的词频统计数组用起来
//由于只需要判断原来字符是否出现过,那么就直接将数组存放为最后一次该元素出现的位置
//需要注意:我们在缩小窗口时左边窗口应该=max(上一次该元素的下标,当前做左窗口位置)因为上一次该字符出现的位置可能已经不在窗口内
//即要保证更新的left值需要保证在本窗口内
public int lengthOfLongestSubstring(String s) {
char[] arr = s.toCharArray();
int[] cnt=new int[255];
Arrays.fill(cnt,-1);
int ans=0;
//默认右窗口移动
for (int l = 0,r=0; l <=r&&r<s.length() ; r++) {
char index=arr[r];
l=Math.max(l,cnt[index]+1);
cnt[index]=r;
ans=Math.max(ans,r-l+1);
}
return ans;
}
}
长度最小的子数组
解析:简单的滑动窗口
唯一需要注意的就是:缩小窗口时的顺序
ans=Math.min(ans,r-l+1);
sum-=nums[l++];
一定是先去更新答案再更新窗口维护的值,如果位置反了,那么就会造成先缩小窗口值造成值已经不够target了,但是我们已经将ans更新了
class Solution {
//思路;:窗口内维护一个窗口内值的和,然后找到窗口最小值即可
public int minSubArrayLen(int target, int[] nums) {
int ans=Integer.MAX_VALUE;
int sum=0;//窗口内维持的值
for (int l = 0,r=0; l <=r&&r<nums.length ; r++) {
//先放入窗口
sum+=nums[r];
//缩小窗口
while (sum>=target&&l<=r){
ans=Math.min(ans,r-l+1);
sum-=nums[l++];
}
}
return ans==Integer.MAX_VALUE?0:ans;
}
}
最小覆盖子串
解析
滑动窗口来解决:只是窗口内维护一张欠债表的词频统计并且维护一个欠的总字符数
窗口移动规则:
- 还欠帐记debate>0那么扩展窗口
- 不欠帐那么尝试更新最小长度
class Solution {
//思路:将目标串转化为一张滑动窗口中的一个欠债表
//当欠债表没还清时就继续扩展窗口
//当欠债表还清了,那么开始考虑缩小窗口,缩小条件弹出元素不属于欠债元素
//0表示不欠,那么就需要一个kind来记录总共前债数量,只有当kind==0时就说明找到了
public String minWindow(String str, String target) {
int len=Integer.MAX_VALUE;
int start=0;//记录最小值起点
int[] cnt=new int[255];
char[] s = str.toCharArray();
char[] t = target.toCharArray();
//更新潜在表
int debate=t.length;
for (int i = 0; i < t.length; i++) {
cnt[t[i]]-=1;
}
//开始滑动
for (int l = 0,r=0; r <s.length ; r++) {
if (cnt[s[r]]++<0){
//是欠债,欠债总数--,如果不是那么就正常+1即可,表示窗口中多出来的
debate--;
}
//如果还完了
while(cnt[s[l]]>0&&l<r){
cnt[s[l++]]--;
}
//到这里如果没欠债那么此时窗口就是当前最小的,尝试更新长度
if (debate==0&&(r-l+1)<len){
start=l;
len=(r-l+1);
}
}
return len==Integer.MAX_VALUE?"":str.substring(start,(start+len));
}
}
加油站
题目描述
解析
这里使用滑动窗口比较巧妙,采用相减的差值来当滑动数组,具体看代码中的思路,需要注意的是,花这里需要保证窗口中维护的值一定要大于等于0,而我们采用的策略是可以先容忍窗口接收一个使得sum<0的值,但是随后马上要缩小窗口所以这里和以前滑动窗口模版所有区别,
while (sum<0&&l<=r)这里的等号是有意义的
其中另一个巧妙点处理:环形数组的处理和赋值,也可以不采用使用双倍数组的方式,解题方法看第二个代码,但是没必要使用方法一不容易错
class Solution {
//思路:这题目主要是判断汽车能否绕一圈,能完成则返回起初出发的起点,不能完成则返回-1
//首先先理解给的数组的作用
//gas[i]:i号加油站最多能加的油
//cost[i]:表示从i~i+1位置消耗
//那我们就直接将两个相减,就得到对应从i~i+1最后到达邮箱剩余油的情况
//得到这个数组后,我们就可以使用滑动窗口来进行处理,还是老样子
//找到窗口中需要维护的值,这里就是维护一个值,这个值不能小于0即可
//但是需要对数组进行特殊处理,因为他可以从任意点当起点,并且要找能绕一圈的
//所以相当于是一个环形数组,一般对环形数组的最好的解决方式就是拷贝一份数组接在数组后面就可以模拟环形数组
public int canCompleteCircuit(int[] gas, int[] cost) {
int[] diff=new int[2*gas.length];
for (int i = 0; i < gas.length; i++) {
diff[i]=gas[i]-cost[i];
//处理环形数组,在i+n位置拷贝一份值
diff[i+gas.length]=diff[i];
}
//开始滑动窗口
int sum=0;//窗口中维护的值
for (int l = 0,r=0; r <diff.length ; r++) {
//窗口中只维护大于等于0的值
sum+=diff[r];
//注意:!!!!!这里的l==r是有意义的,因为要保证窗口中维护的值一定>=0那么当进了一个小于0的数到🪟中,就应该立即把这个数移出来
while (sum<0&&l<=r){
sum-=diff[l++];
};
if (sum>=0&&r-l+1==gas.length){
return l;
}
}
return -1;
}
}
方法二:执行速度快于方法一:
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n=gas.length;
for (int l = 0,r=0,sum=0,len=0; l <n ; l++) {
//l,r:分别为当前窗口的左右边界
//sum:表示当前窗口的累加和
//len表示当前窗口的大小
while (sum>=0){
if(len==n){
//如果现在已经长度==总加油站长度则返回窗口左值
return l;
}
// 否则只要当前窗口的累加和>=0则尝试往下扩
r=(l+len)%n;//由于该数组我们需要看成环形数组因此需要取模操作
len++;
sum+=gas[r]-cost[r];//gas[r]-cost[r];表示当前加油站到下一个加油站的余量
}
//运行到这里说明sum<0,表示以当前l为左边界的窗口已经不能在扩了,且还没找到答案,因此需要缩小串口即要执行for循环中的l++了
//因此将当前l对应再窗口中的值移除
sum-=gas[l]-cost[l];
len--;
}
//遍历完还没找到结果则返回-1
return -1;
}
}
替换子串得到平衡字符串
题目描述
解析
这题和之前做滑动窗口的题目习惯不太一样,之前做滑动窗口题目都习惯默认移动右边窗口来做,但是这题移动右边窗口不太好做,这里采用移动左窗口的方法,类似于枚举每个左窗口,然后移动右窗口来找满足要求的
class Solution {
//思路:
//首先题目要我们求的是替换一个子串使得总的字符串中每个字符出现的次数相等
//那么我们可以先进行一个词频统计看看哪些字符多,哪些字符少
//由于本题是由子串进行覆盖,所以同样考虑滑动窗口
//窗口中维护的值为:自由窗口值,就是这个窗口中长度中的字符都是可以支配的字符,哪个字符数量少于1/4 就可以进行补偿
//窗口扩充条件:只要窗口补偿不完那么就扩展
//缩小窗口条件:默认每次循环l++,这时寻找每个l满足条件的len长度,可以枚举完所有情况·
public int balancedString(String s) {
char[] arr = s.toCharArray();
int n=s.length();
//字符映射
int[] smap=new int[n];
//每个字符应该有的个数
int require=n/4;
//字符映射于词频统计
int[] cnt=new int[4];
for (int i = 0; i < n; i++) {
//只会出现4种字符
if (arr[i]=='Q') smap[i]=0;
else if (arr[i]=='W') smap[i]=1;
else if (arr[i]=='E')smap[i]=2;
else smap[i]=3;
cnt[smap[i]]++;
}
int ans=Integer.MAX_VALUE;
//滑动,这题使用移动左边界比较好做,默认移动左边界,
//然后如果不满足条件,就while右边界,这道满足要求就得到当前l的最小长度
//如果还是像以前默认移动右边界,那么不好对左边界移动判断,
//因为右边界移动满足后,即使用右边界当基准,即每个移动的右边界都能美剧到
//最小长度并不好操作
for (int l= 0,r=0; l <n ; l++) {
//看看当前l~r能否满足不满足移动右边界,因为可能存在不需要替换操作,
//即原本数组就是平衡的,所以枚举的窗口大小设置为0
//最大的窗口长度最多也就是n-1,因为窗口长度代表能够自由支配的长度
//,比如qqqq,那么最多窗口长度就是3
while (!ok(cnt,require,r-l)&&r<n){
cnt[smap[r++]]--;
}
//再次判断是否满足,不能使用r<n,因为,
//上面判断ok是否满足时第一次满足r-l是最小的,
//但是while还会继续判断下一次循环即r++来继续判断是否满足,
//所以以r<n来判断的话回事r-l长度可能增加1
if (ok(cnt,require,r-l)){
ans=Math.min(ans,r-l);
}
//左窗口移动加字符,那么本次的左窗口就是自由窗口之外的,
//那么总体的cnt就需要
//加上这个窗口的字符
cnt[smap[l]]++;
}
return ans;
}
private boolean ok(int[] cnt, int require, int len) {
for (int i = 0; i < 4; i++) {
if (cnt[i]>require) return false;
len-=require-cnt[i];
}
return len==0;
}
}