本文参考:
Leetcode刷题之滑块窗口算法总结
labuladong的滑动窗口专题
滑动窗口力扣总结
Leetcode算法总结–滑动窗口类算法解析
Leetcode刷题之滑块窗口算法总结
滑动窗口专题三大题型汇总+通用模板:持续更新
滑动窗口主要用来处理连续问题。一般这些题目的特征可归纳为:求满足XXX条件(一般这些条件为满足什么样的计算结果、出现多少次、不包含重复字符、包含XXX等条件)最长/最短的子串(必须连续)、子序列(可以不连续,但是可以对其中不连续部分进行替换处理)、子数组(通常连续)。
但要注意,滑动窗口要注意「窗口中的res
随right右移
的递增性」,根据窗口内局部变量res的大小调整left、right
,大了left向左
让res变小,小了right向右
让res增大。比如求和要求元素都大于等于0,如果数据中有负数,会破坏单调性,right右移
加入了一个负数,res变小
;或者求和偏大了,left左移
移除了一个负数,和更大了,这就不能应用滑动窗口了。
结合相关题目抽象出以下模板,需要注意区分:最小值合法收缩还是最大值非法收缩,合法收缩内部捕获结果,非法收缩外部捕获结果。同时需要验证right右移
,局部变量res
递增的合法性。
从类型上可以分为以下几种类型:
while
窗口非法->内部left
收缩,「外部捕获结果」更新状态
感性理解为:window一非法,就需要移除window中所有的非法元素,因此合法的结果在外部。当具有去重属性的时候,通常需要在窗口判断中先运用window right
。
HashSet<Character> set=new HashSet<>();
char[] str=s.toCharArray();
int len=str.length;
int left=0;
int ans=0;
//1.右边界right逐个加入
for(int right=0;right<len;right++){
//2. 窗口非法,while收缩左边界
while(set.contains(str[right])){
set.remove(str[left++]);
}
//3. right加入窗口
set.add(str[right]);
//4. 更新res状态,外部捕获
int res=right-left+1;
ans=Math.max(ans,res);
}
return ans;
while
窗口合法->内部left
收缩,「内部捕获结果」更新状态
感性理解为:一有满足条件的合法window,就对其进行优化收缩,因此合法结果在内部。
int sum=0;
int len=nums.length;
int left=0;
int ans=Integer.MAX_VALUE;
//1.右边界right逐个加入
for(int right=0;right<len;right++){
//2. right加入窗口
sum+=nums[right];
//3. 窗口合法,while收缩左边界
while(sum>=target){
int res=right-left+1;
//4. 更新res状态, 内部捕获
ans=Math.min(res,ans);
sum-=nums[left++];
}
}
return ans==Integer.MAX_VALUE?0:ans;
if
窗口刚好满足条件->内部left
收缩,「内部捕获结果」更新状态
int left=0;
int len=nums.length;
double ans=Integer.MIN_VALUE;
double sum=0;
//1.右边界right逐个加入
for(int right=0;right<len;right++){
//2. right加入窗口
sum+=nums[right];
//3. 窗口刚好,if收缩左边界
if(right-left+1==k){
//4. 更新状态,内部捕获
double res=sum/k;
ans=Math.max(ans,res);
sum-=nums[left++];
}
}
return ans;
无通用模板。
滑动窗口中需要明确几个变量和概念:
left、right
根据条件进行移动,window
用于存储左右指针中间的值,根据题目的限制条件,可能会使用数组、HashSet、HashMap等数据结构作为窗口。心得体会:
nums[right]
使用一般在while/if
判断之前,nums[right]
加入窗口一般非法在while
之后,合法在while
之前,然而window right++
更新可能发生在之前或者之后,for循环
是放在最后更新right++
,而有些必须在while/if
判断之前就使用window right并right++
,如1248.统计优美子数组。window需要具有去重属性,同时题目要求找到最大子串,不满足条件时进行window收缩。
两种做法,采用HashSet结合while不断进行重复元素的去除:
class Solution {
public int lengthOfLongestSubstring(String s) {
HashSet<Character> set=new HashSet<>();
char[] str=s.toCharArray();
int len=str.length;
int left=0;
int ans=0;
for(int right=0;right<len;right++){
//采用while条件将重复的字符一个个去除
while(set.contains(str[right])){
set.remove(str[left++]);
}
// window right在非法判断后加入
set.add(str[right]);
int res=right-left+1;
ans=Math.max(ans,res);
}
return ans;
}
}
采用HashMap记录,key为字符元素,Value为元素出现最后出现的位置,if条件判断重复,left跳到最后出现位置的后一个位置。
class Solution {
public int lengthOfLongestSubstring(String s) {
char[]str=s.toCharArray();
int len=str.length;
int left=0;
int ans=0;
HashMap<Character,Integer> map=new HashMap<>();
for(int right=0;right<len;right++){
if(map.containsKey(str[right])){
//要不断向右更新,取最大值
left=Math.max(left,map.get(str[right])+1);
}
map.put(str[right],right);
int res=right-left+1;
ans=Math.max(ans,res);
}
return ans;
}
}
删除子数组的最大值,指的是删除数组的得分之和,并且删除数组连续不重复。因此window需要具备去重属性,并且需要一个局部变量记录window之和。
class Solution {
public int maximumUniqueSubarray(int[] nums) {
int ans=0;
HashSet<Integer> set=new HashSet<>();
int len=nums.length;
int left=0;
int sum=0;
for(int right=0;right<len;right++){
while(set.contains(nums[right])){
sum-=nums[left];
set.remove(nums[left++]);
}
//window right在非法判断后加入
set.add(nums[right]);
sum+=nums[right];
ans=Math.max(ans,sum);
}
return ans;
}
}
本题的关注点在于,两个字符串中可以转换的最大长度的连续字符串,求其最大长度,可以通过滑动窗口进行连续求解。
class Solution {
public int equalSubstring(String s, String t, int maxCost) {
int left=0;
int right;
int sum=0;
int ans=0;
int len=s.length();
for(right=0;right<len;right++){
//window right在非法判断前就加入
sum+=Math.abs(s.charAt(right)-t.charAt(right));
while(sum>maxCost){
sum-=Math.abs(s.charAt(left)-t.charAt(left));
left++;
}
int res=right-left+1;
ans=Math.max(ans,res);
}
return ans;
}
}
运用贪心思想,遇到0先用k
的次数抵消,直到k<0
则开始收缩left
,在非法判断外进行结果更新。
class Solution {
public int longestOnes(int[] nums, int k) {
int left=0;
int len=nums.length;
int ans=0;
for(int right=0;right<len;right++){
//先运用右边界
if(nums[right]==0) k--;
//非法判断
while(k<0){
if(nums[left++]==0) k++;
}
//结果更新
int res=right-left+1;
ans=Math.max(ans,res);
}
return ans;
}
}
滑动窗口+位运算。用一个数字窗口window
维护整型32位上出现的数字,每次移动right只需要判断当前数字与窗口中的&
以后,是否为0,如果为0则代表所有数字都散落在窗口的不同位置,如果不为0则需要收缩左边界。可以保证的是,如果当前的数字与窗口&只为0才加入,那么窗口中所有的数字互相&都是0.
class Solution {
public int longestNiceSubarray(int[] nums) {
int res=0;
int len=nums.length;
int window=0;
int left=0;
int right=0;
while(right<len){
int temp=window & nums[right];
while(temp!=0){
window ^= nums[left];
temp = window & nums[right];
left++;
}
window |= nums[right];
res=Math.max(res,right-left+1);
right++;
}
return res;
}
}
滑动窗口最大值+哈希。用哈希表defaultdict记录现在统计过的每组的个数,用group统计当前总共的组数,当组数>2的时候,左指针开始移动,直到有一组的个数为0,组数-1满足条件。
class Solution:
def totalFruit(self, fruits: List[int]) -> int:
left,right,n,group,res=0,0,len(fruits),0,0
# 用defaultdict记录每组的个数
cnt=defaultdict(int)
while right<n:
cnt[fruits[right]]+=1
if cnt[fruits[right]]==1:
group +=1
# 当已经有的组数>2,则需要移动左指针直到有一组个数变为0,组数-1
while group>2:
cnt[fruits[left]] -=1
if cnt[fruits[left]] ==0:
group -=1
left +=1
res = max(res, right-left +1)
right +=1
return res
sum需要记录window中的总和,res进行数组长度更新,当条件满足时需要进行收缩。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int sum=0;
int len=nums.length;
int left=0;
int ans=Integer.MAX_VALUE;
for(int right=0;right<len;right++){
// window right在合法判断前就加入
sum+=nums[right];
while(sum>=target){
int res=right-left+1;
ans=Math.min(res,ans);
sum-=nums[left++];
}
}
return ans==Integer.MAX_VALUE?0:ans;
}
}
这题先要明确求的是替换子串的最小长度,而不关心究竟是如何进行替换的。我们先遍历一遍,将每个字符的数量统计出来。由于字符数量一定有多有少,且最后一定能够得到平衡字符串,然后只需要着眼于在当前子串窗口内,是否能够将多出的字符(可能有几个)替换掉,即满足当前状态下,所有字符数量都小于等于average
,因为这样肯定能够将多的补给少的。
比方说,QWQQWQQQWEEWRQRR 其中Q7、W4、E2、R3,第一次窗口是 QWQQ,得到Q4、W3、E2、R3,把这个替换成WWER 就可以满足平衡字符串,因此满足平衡串的条件,进一步收缩窗口。
class Solution {
public int balancedString(String s) {
int[]cnt=new int[26];
for(char c:s.toCharArray()){
cnt[c-'A']++;
}
int left=0;
int len=s.length();
int ans=len;
int average=len/4;
//特判本来就是平衡串
if(cnt['Q'-'A']==average&&cnt['W'-'A']==average&&cnt['E'-'A']==average&&cnt['R'-'A']==average) return 0;
for(int right=0;right<len;right++){
// window right在判断前加入,会对本来就是平衡字符串的产生影响,需要加个特判
cnt[s.charAt(right)-'A']--;
//合法判断,内部收缩
while(cnt['Q'-'A']<=average&&cnt['W'-'A']<=average&&cnt['E'-'A']<=average&&cnt['R'-'A']<=average){
ans=Math.min(ans,right-left+1);
cnt[s.charAt(left)-'A']++;
left++;
}
}
return ans;
}
}
滑动窗口最小值
方法类似于 438.找到字符串中所有字母异位词,用target数组
记录要匹配的值,大小写加上中间的字母总共58的大小。用check()
检查是否包含,包含则调整左边界,缩小字符串长度。
class Solution {
public String minWindow(String s, String t) {
int[]target=new int[58];
for(char c:t.toCharArray()){
target[c-'A']++;
}
String res="";
int left=0;
int len=s.length();
int[]temp=new int[58];
int ans=0x3f3f3f3f;
for(int right=0;right<len;right++){
temp[s.charAt(right)-'A']++;
while(check(temp,target)){
int value=right-left+1;
if(value<ans){
ans=value;
res=s.substring(left,right+1);
}
temp[s.charAt(left)-'A']--;
left++;
}
}
return res;
}
public boolean check(int[]temp,int[]target){
for(int i=0;i<temp.length;i++){
if(temp[i]<target[i]) return false;
}
return true;
}
}
滑动窗口求最小值,下面采用数组记录大小写字母的频次
class Solution {
public String minWindow(String s, String t) {
int len=s.length();
int[]mask=new int[58]; // 用来记录大小写字母出现的频次
int[]win=new int[58];
int ansv=0x3f3f3f3f;
String anss="";
for(char c:t.toCharArray()){
mask[c-'A']++;
}
int left=0;
for(int right=0;right<len;right++){
win[s.charAt(right)-'A']++;
while(check(win,mask)){ // while 不断缩小左边界
if(right-left+1<ansv){
ansv=Math.min(ansv,right-left+1);
anss=s.substring(left,right+1);
}
win[s.charAt(left++)-'A']--;
}
}
return anss;
}
// 检验涵盖条件
public boolean check(int[]win,int[]mask){
for(int i=0;i<58;i++){
if(win[i]<mask[i]) return false;
}
return true;
}
}
可以将其改为哈希表记录字母出现频次
class Solution {
public String minWindow(String s, String t) {
int len=s.length();
HashMap<Character,Integer>mask=new HashMap<>(); // 用来记录大小写字母出现的频次
HashMap<Character,Integer>win=new HashMap<>();
int ansv=0x3f3f3f3f;
String anss="";
for(char c:t.toCharArray()){
mask.put(c,mask.getOrDefault(c,0)+1);
}
int left=0;
for(int right=0;right<len;right++){
win.put(s.charAt(right),win.getOrDefault(s.charAt(right),0)+1);
while(check(win,mask)){ // while 不断缩小左边界
if(right-left+1<ansv){
ansv=Math.min(ansv,right-left+1);
anss=s.substring(left,right+1);
}
win.put(s.charAt(left),win.get(s.charAt(left))-1);
left++;
}
}
return anss;
}
// 检验涵盖条件
public boolean check(HashMap<Character,Integer>win,HashMap<Character,Integer>mask){
for(Map.Entry<Character,Integer> e:mask.entrySet()){
if(!win.containsKey(e.getKey())) return false;
else if(e.getValue()>win.get(e.getKey())) return false;
}
return true;
}
}
上面在每次窗口移动中都需要用一个循环,遍历mask中的所有字符,进行涵盖的判断,可以将这一步放在窗口移动中,用一个变量记录已经涵盖的字符数。
class Solution {
public String minWindow(String s, String t) {
int len=s.length();
HashMap<Character,Integer>mask=new HashMap<>(); // 用来记录大小写字母出现的频次
HashMap<Character,Integer>win=new HashMap<>();
int ansv=0x3f3f3f3f;
String anss="";
for(char c:t.toCharArray()){
mask.put(c,mask.getOrDefault(c,0)+1);
}
int valid=0; // 用来记录win中已经涵盖t的字符数
int left=0;
for(int right=0;right<len;right++){
char c=s.charAt(right);
win.put(c,win.getOrDefault(c,0)+1);
// 只能刚好相等的时候++,不然重复了,并且两个Integer相等只能用.equals(),原因是-128-127缓存
if(mask.containsKey(c)&&win.get(c).equals(mask.get(c))) valid++;
while(valid==mask.size()){ // while 不断缩小左边界
if(right-left+1<ansv){
ansv=Math.min(ansv,right-left+1);
anss=s.substring(left,right+1);
}
char d=s.charAt(left);
win.put(d,win.get(d)-1);
// 这里可以直接写<,因为valid字符就是独特独特的一个字符,不存在重复
if(mask.containsKey(d)&&win.get(d)<mask.get(d)) valid--;
left++;
}
}
return anss;
}
}
if条件判断当且仅当窗口大小等于长度k,本次计算以后进行左边界收缩。
class Solution {
public double findMaxAverage(int[] nums, int k) {
int left=0;
int len=nums.length;
double ans=Integer.MIN_VALUE;
double sum=0;
for(int right=0;right<len;right++){
//window right在合法判断前加入
sum+=nums[right];
if(right-left+1==k){
double res=sum/k;
ans=Math.max(ans,res);
sum-=nums[left++];
}
}
return ans;
}
}
固定窗口大小为p的长度,用Arrays.equals
通过计数数组进行窗口内的元素检查,当且仅当p的单词都在窗口内出现过,left
加入ans
。其中需要注意,在窗口大小等于k
时,即使窗口内的计数数组匹配不成功,也需要调整左边界。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer>ans=new ArrayList<>();
int[] target=new int[26];
for(char c:p.toCharArray()){
target[c-'a']++;
}
int left=0;
//标记出现字母出现次数的数组作为窗口
int[]temp=new int[26];
int k=p.length();
int len=s.length();
for(int right=0;right<len;right++){
temp[s.charAt(right)-'a']++;
//合法判断
if(right-left+1==k){
//判断两数组是否相等,调用Arrays.equals
if(Arrays.equals(target,temp)){
ans.add(left);
}
//状态更新在长度为k时,无论成功与否,都需要调整左边界
temp[s.charAt(left)-'a']--;
left++;
}
}
return ans;
}
}
解题方法类似438.找到字符串中所有字母异位词,只不过变成了判断是否有子串问题。
class Solution {
public boolean checkInclusion(String s1, String s2) {
int k=s1.length();
int len=s2.length();
int left=0;
int[]target=new int[26];
for(char c:s1.toCharArray()){
target[c-'a']++;
}
int[]window=new int[26];
for(int right=0;right<len;right++){
window[s2.charAt(right)-'a']++;
if(right-left+1==k){
if(Arrays.equals(window,target)) return true;
window[s2.charAt(left)-'a']--;
left++;
}
}
return false;
}
}
参考题解,应用了滑动窗口思想,但是是通过每次left或者right
的改变求满足条件的连续子数组数量。不满足条件收缩,外部捕获结果。
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
int ans = 0;
int left = 0;
int right=0;
int len = nums.size();
int mul = 1;
while (right < len) {
mul *= nums[right];
// 不满足条件的时候
while(left<=right&&mul >= k){
mul /= nums[left];
left++;
}
ans += right - left + 1;
right++;
}
return ans;
}
};
参考甜姨的题解,滑动窗口求解个数。
class Solution {
public int numberOfSubarrays(int[] nums, int k) {
int len=nums.length;
int ans=0;
int left=0;
int right=0;
int oddcnt=0;
while(right<len){
//right++必须写在这里,因为在oddcnt==k情况中,最终right一定会到下一个奇数的位置,所以这里++无所谓,但如果最后++,会多走一个
//判断中更新,即使不满足==1也会right++
if((nums[right++]&1)==1){
oddcnt++;
}
if(oddcnt==k){
//找到右边下一个奇数,最终停在该奇数上,或者right
int rightcnt=0;
while(right<len&&nums[right]%2==0){
right++;
rightcnt++;
}
//找到左边第一个奇数
int leftcnt=0;
while(left<right&&nums[left]%2==0){
left++;
leftcnt++;
}
//leftcnt,rightcnt是偶数个数,加1是需要包含第一个奇数本身,不算左右那些偶数
ans+=(leftcnt+1)*(rightcnt+1);
//更新左边界,继续往右搜索,右边界已经在下一个奇数的位置了
left++;
oddcnt--;
}
}
return ans;
}
}