我们平常做题的时候只要看到是和子串相关的题目我们就要第一时间想到是不是滑动窗口题目,一般的子串都是用滑动窗口和双指针进行解决,还有一个前缀和。
固定窗口
滑动窗口主要关注三个量:窗口大小,左边界,右边界
定义两个指针简化操作运算,固定窗口当窗口建成后,每次left和right向右移动一步即可,但是要更新中间变量
固定窗口模板:
1. 定义需要的中间变量/res数组,滑动窗口左指针、右指针
常用有(unordered_set、unordered_map)(left,right)
2. 使用右指针遍历字符串/数组,右指针向前延伸窗口
for(right=0;right<nums.size();right++){
}
3. 在for循环内部,根据题意存入符合题意的结果,并在合适的时候用左指针来收缩窗口
(注意先更新left指针维护的变量,再将left指针右移)
if(tmp.count(nums[right])!=0) return true;
tmp.insert(nums[right]);
if((right-left)==k){
tmp.erase(nums[left]);
left++;
}
固定窗口题目:
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
固定窗口优化
固定窗口,窗口大小不变,所以可以只使用一个变量来遍历,右指针:i,左指针:i-pize+1(pize:窗口大小)
(。。。纯纯有病,没必要这么扣内存吧)
滑动窗口解题框架:
step 1. 定义需要维护的变量们(对于滑动窗口类题目,这些变量通常是最小长度、最大长度或哈希表)
step 2.定义窗口首尾端(start,end)然后滑动窗口
step 3.更新需要维护的变量,有的变量需要一个if语句来维护(比如最大最小长度)
—————————————以下两种情况需根据题意进行选择——————————————
step 4.——情况1
题目窗口长度固定:用一个if语句判断当前窗口长度是否达到限定长度;
达到了限定长度时:
更新step 1定义的(部分或所有)维护变量;
窗口左指针前移一个单位,保证下一次右指针右移时,窗口长度不变;
step 4.——情况2
题目窗口长度可变:此时一般涉及到窗口是否合法的问题;
窗口不合法时:
更新step 1定义的(部分或所有)维护变量;
使用while不断移动窗口左指针,从而剔除非法元素直到窗口再次合法;
for(int i=0;i<nums.size();i++){
while(){
}
}
step 5.返回答案
变长窗口
最小滑窗:先更新,再if判断是否满足,满足中使用while不断缩小左区间
最大滑窗:先while判断是否满足,满足后跳出并更新
最大滑窗是在迭代右移右边界的过程中更新结果,而最小滑窗是在迭代右移左边界的过程中更新结果。
最大滑窗:while 判断不满足条件,满足后跳出,在 while 外更新结果
最小滑窗:while 判断满足条件,在 while 里面满足就更新结果,并且在 while 里更新完即缩小左指针
最小滑窗模板:给定数组 nums,定义滑窗的左右边界 i, j,求满足某个条件的滑窗的最小长度。
while j < len(nums):
判断[i, j]是否满足条件
while 满足条件:
不断更新结果(注意在while内更新!)
i += 1 (最大程度的压缩i,使得滑窗尽可能的小)
j += 1
for(;right<nums.size();right++){
例题:
最大滑窗模板:给定数组 nums,定义滑窗的左右边界 i, j,求满足某个条件的滑窗的最大长度。
for(;right<nums.size();right++){
while(不满足条件){
更新(移除)left所维护的变量;
left++;//最保守地压缩窗口,一旦满足条件就退出压缩过程,使窗口尽可能大
}
在while循环外更新返回结果或中间变量;
更新和right有关所维护的变量;
}
713. 乘积小于 K 的子数组 - 力扣(LeetCode)
1004. 最大连续1的个数 III - 力扣(LeetCode)
关键的区别在于,最大滑窗是在迭代右移右边界的过程中更新结果,而最小滑窗是在迭代右移左边界的过程中更新结果。因此虽然都是滑窗,但是两者的模板和对应的贪心思路并不一样,而真正理解后就可以在lc.76,lc.904,lc.3, lc.1004写出非常无脑的代码。
—————————————————自写————————————————————
————————————————负雪明烛——————————————————
《挑战程序设计竞赛》这本书中把滑动窗口叫做「虫取法」,我觉得非常生动形象。因为滑动窗口的两个指针移动的过程和虫子爬动的过程非常像:前脚不动,把后脚移动过来;后脚不动,把前脚向前移动。
分享一个滑动窗口的模板,能解决大多数的滑动窗口问题:
def findSubArray(nums):
N = len(nums) # 数组/字符串长度
left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
sums = 0 # 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
sums += nums[right] # 增加当前右边指针的数字/字符的求和/计数
while 区间[left, right]不符合题意: # 此时需要一直移动左指针,直至找到一个符合题意的区间
sums -= nums[left] # 移动左指针前需要从counter中减少left位置字符的求和/计数
left += 1 # 真正的移动左指针,注意不能跟上面一行代码写反
# 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
res = max(res, right - left + 1) # 需要更新结果
right += 1 # 移动右指针,去探索新的区间
return res
滑动窗口中用到了左右两个指针,它们移动的思路是:以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。
模板的整体思想是:
定义两个指针 left 和 right 分别指向区间的开头和结尾,注意是闭区间;定义 sums 用来统计该区间内的各个字符出现次数;
第一重 while 循环是为了判断 right 指针的位置是否超出了数组边界;当 right 每次到了新位置,需要增加 right 指针的求和/计数;
第二重 while 循环是让 left 指针向右移动到 [left, right] 区间符合题意的位置;当 left 每次移动到了新位置,需要减少 left 指针的求和/计数;
在第二重 while 循环之后,成功找到了一个符合题意的 [left, right] 区间,题目要求最大的区间长度,因此更新 res 为 max(res, 当前区间的长度) 。
right 指针每次向右移动一步,开始探索新的区间。
模板中的 sums 需要根据题目意思具体去修改,本题是求和题目因此把sums 定义成整数用于求和;如果是计数题目,就需要改成字典用于计数。当左右指针发生变化的时候,都需要更新 sums 。
另外一个需要根据题目去修改的是内层 while 循环的判断条件,即: 区间 [left,right][left, right][left,right] 不符合题意 。对于本题而言,就是该区间内的 0 的个数 超过了 2 。
题目汇总: