算法学习-滑动窗口

文章目录

  • 基础知识
    • 算法模板
      • (1)窗口长度可变求最大值
      • (2)窗口长度可变求最小值
      • (3)窗口长度固定求满足条件的解
      • (4)应用滑动窗口,但不求最值
  • 相关题目
    • (1)窗口长度可变求最大值
        • 3.无重复字符的最长子串
        • 1695.删除子数组的最大得分
        • 1208.尽可能使字符串相等
        • 1004.最大连续1的个数III
        • 2401.最长优雅子数组
        • 904.水果成篮
    • (2)窗口长度可变求最小值
        • 209.长度最小的子数组
        • 1234.替换子串得到平衡字符串
        • 17.含有所有字符的最短字符
        • 76.最小覆盖子串
    • (3)窗口长度固定求满足条件的解
        • 643.子数组最大平均数I
        • 438.找到字符串中所有字母异位词
        • 567.字符串的排列
    • (4)应用滑动窗口,但不求最值
        • 9.乘积小于K的子数组
        • 1248.统计优美子数组

基础知识

本文参考:
Leetcode刷题之滑块窗口算法总结
labuladong的滑动窗口专题
滑动窗口力扣总结
Leetcode算法总结–滑动窗口类算法解析
Leetcode刷题之滑块窗口算法总结
滑动窗口专题三大题型汇总+通用模板:持续更新

滑动窗口主要用来处理连续问题。一般这些题目的特征可归纳为:求满足XXX条件(一般这些条件为满足什么样的计算结果、出现多少次、不包含重复字符、包含XXX等条件)最长/最短子串(必须连续)、子序列(可以不连续,但是可以对其中不连续部分进行替换处理)、子数组(通常连续)

但要注意,滑动窗口要注意「窗口中的resright右移的递增性」,根据窗口内局部变量res的大小调整left、right,大了left向左res变小,小了right向右res增大。比如求和要求元素都大于等于0,如果数据中有负数,会破坏单调性,right右移加入了一个负数,res变小;或者求和偏大了,left左移移除了一个负数,和更大了,这就不能应用滑动窗口了。

算法模板

结合相关题目抽象出以下模板,需要注意区分:最小值合法收缩还是最大值非法收缩,合法收缩内部捕获结果,非法收缩外部捕获结果同时需要验证right右移,局部变量res递增的合法性。

类型上可以分为以下几种类型:

  1. 窗口大小不固定,求解满足条件的最大值
  2. 窗口大小不固定,求解满足条件的最小值
  3. 窗口大小固定,求满足条件的解
  4. 应用滑动窗口,但不求最值

(1)窗口长度可变求最大值

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;

(2)窗口长度可变求最小值

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;

(3)窗口长度固定求满足条件的解

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;

(4)应用滑动窗口,但不求最值

无通用模板。

相关题目

滑动窗口中需要明确几个变量和概念:

  1. 窗口两端的左右指针left、right以及其围成的窗口windowleft、right根据条件进行移动,window用于存储左右指针中间的值,根据题目的限制条件,可能会使用数组、HashSet、HashMap等数据结构作为窗口。
  2. 记录当前窗口信息的一个或多个局部变量res,可以是平均数、最大长度、最小长度等等,根据窗口收缩不断更新。但有些固定窗口是找出符合条件的值。
  3. 记录最终结果的ans,在上面每一次窗口更新以后,选出其中会对结果产生影响的变量更新结果。
  4. 确定窗口收缩的时机最为关键,需要分类讨论,最小值合法收缩还是最大值非法收缩,合法收缩内部捕获结果,非法收缩外部捕获结果

心得体会:

  1. 对于nums[right]使用一般在while/if判断之前,nums[right]加入窗口一般非法在while之后,合法在while之前,然而window right++更新可能发生在之前或者之后,for循环是放在最后更新right++,而有些必须在while/if判断之前就使用window right并right++,如1248.统计优美子数组。

(1)窗口长度可变求最大值

3.无重复字符的最长子串

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;
    }
}
1695.删除子数组的最大得分

删除子数组的最大值,指的是删除数组的得分之和,并且删除数组连续不重复。因此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;
    }
}
1208.尽可能使字符串相等

本题的关注点在于,两个字符串中可以转换的最大长度的连续字符串,求其最大长度,可以通过滑动窗口进行连续求解。

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;
    }
}
1004.最大连续1的个数III

运用贪心思想,遇到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;
    }
}
2401.最长优雅子数组

滑动窗口+位运算。用一个数字窗口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;
    }
}
904.水果成篮

滑动窗口最大值+哈希。用哈希表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

(2)窗口长度可变求最小值

209.长度最小的子数组

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;
    }
}
1234.替换子串得到平衡字符串

这题先要明确求的是替换子串的最小长度,而不关心究竟是如何进行替换的。我们先遍历一遍,将每个字符的数量统计出来。由于字符数量一定有多有少,且最后一定能够得到平衡字符串,然后只需要着眼于在当前子串窗口内,是否能够将多出的字符(可能有几个)替换掉,即满足当前状态下,所有字符数量都小于等于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;
    }
}
17.含有所有字符的最短字符

滑动窗口最小值
方法类似于 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;
    }
}
76.最小覆盖子串

滑动窗口求最小值,下面采用数组记录大小写字母的频次

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;
    }

   
}

(3)窗口长度固定求满足条件的解

643.子数组最大平均数I

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;
    }
}
438.找到字符串中所有字母异位词

固定窗口大小为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;
    }
}
567.字符串的排列

解题方法类似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;
    }
}

(4)应用滑动窗口,但不求最值

9.乘积小于K的子数组

参考题解,应用了滑动窗口思想,但是是通过每次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;
    }
};
1248.统计优美子数组

参考甜姨的题解,滑动窗口求解个数。

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;
    }
}

你可能感兴趣的:(算法人生,算法,leetcode,java,数据结构)