滑动窗口篇

目录

滑动窗口的应用 

滑动窗口的核心框架

76最小覆盖字串

438找到字符串所有的字母异位词

239滑动窗口的最大值

剑指 Offer 57 - II. 和为s的连续正数序列


滑动窗口的应用 

滑动窗口的核心框架

/* 滑动窗口算法框架 */
void slidingWindow(string s) {
    HashMap<char, int> window;
    
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s.charAt(i);
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s.charAt(i);
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}
  • 滑动窗口的最大应用就是子串问题
  • 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。

    PS:理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。两种情况都会给边界处理带来不必要的麻烦。

    2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

    3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

    4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

76最小覆盖字串

package algorithm.Arrays.slideWindow;

import java.util.HashMap;

public class Test76 {
    public String minWindow(String s, String t) {
        HashMap<Character,Integer> window=new HashMap<>();//滑动窗口
        HashMap<Character,Integer> need=new HashMap<>();
        //need 用来存储我们需要的字符和对应的需要的字符的个数
        for (int i = 0; i < t.length(); i++) {
             char c=t.charAt(i);
             need.put(c,need.getOrDefault(c,0)+1);
        }
        int left=0;
        int right=0;
        //区间的左闭右开的
        int valid=0;//满足条件的字符个数
        int start=0,len=Integer.MAX_VALUE;//用来确定符合条件的字符串
        while (right<s.length()){
            //c是即将要移入的字符
            char c=s.charAt(right);
            //右移 扩大窗口
            right++;
            //这一部分是模板,用于我们移动右指针,增大窗口
            //-------------------------------------
            if (need.containsKey(c)){
                window.put(c,window.getOrDefault(c,0)+1);
                if (window.get(c).equals(need.get(c))){
                    valid++;
                }
            }
            //这一部分是因题目不同,对窗口内的数据进行更新(新填一个字符,可能要更新对应map(window)数据)
            //-------------------------------------
            while (valid==need.size()){
                //说明了当前我们的窗口的字符串已经满足了条件
                //以下用来更新左边的范围
                if (right-left<len){
                    start=left;
                    len=right-left;
                    //说明存在更短的窗口大小
                }
                
                //------------------
                //d是要移出去的字符
                char d=s.charAt(left);
                //将left这个字符从窗口移除
                left++;
                //满足条件,我们必须开始减少窗口的大小,这也滑动窗口的模板
                
                //-------------------
                if (need.containsKey(d)){
                    //如果删除的这个字符是need里面的字符
                    if (window.get(d).equals(need.get(d))){
                        //如果现在窗口中这个字符的数量跟我们需要的字符数量一样,那么减去这个,这个字符就不满足要求了
                        valid--;
                    }
                    window.put(d,window.getOrDefault(d,0)-1);
                }
                //减少一个数据,有可能会让我们对应的map(need)数据会变化,不同的题目会有不同的情况
                
            }
        }
        return len==Integer.MAX_VALUE?"":s.substring(start,start+len);
    }
}
  • 首先套核心的滑动窗口套路,左闭右开的窗口区间,left从0开始,right从0开始
  • 当不满足条件的时候,首先一直让窗口变大(right++),直到满足了条件,当满足了条件,我们就一直减少窗口的大小(left--),直到不满足了条件,我们就继续让窗口变大,一直重复,直到right走到了数组的尽头
  • 不同的就是关于,如何去判断是否满足了条件,和记录我们要保留的数据,这道题的判断,是靠两个map,window来记录窗口中字符和其对应的个数,need来记录我们需要的字符和其对应的个数,用vaild来记录我们满足的字符的个数,如果vaild==need.length,说明满足条件,小于就不满足
  • 这里面有一个Java语法的注意点,就是Interger这是一个包装类,切回缓存频繁使用的数值,数值范围是-128到127,在次范围内会直接返回缓存值,如果超过了这个范围,会新new一个对象,不能用等于号,用equlas;
 Integer a1 = 1234;
        Integer a2 = 1234;
        System.out.println(a1 == a2);//false
        System.out.println(a1.equals(a2));//true

        Integer a3 = 123;
        Integer a4 = 123;
        System.out.println(a3 == a4);//true
        System.out.println(a3.equals(a4));//true

438找到字符串所有的字母异位词

class Solution {
       public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res=new LinkedList<>();//用于存储我们的结果
        HashMap<Character,Integer> window=new HashMap<>();//窗口的存储的信息
        HashMap<Character,Integer> need=new HashMap<>();
        //存储需要的对应的信息
        for (int i = 0; i < p.length(); i++) {
            char c=p.charAt(i);
            need.put(c,need.getOrDefault(c,0)+1);
        }
        int left=0;
        int right=0;
        //定义左右指针
        int valid=0;//我们这里维护的应该是一个定长的区间,如果
        while (right<s.length()){
            char c=s.charAt(right);//当前我们要进行插入的字符
            right++;
            if (need.containsKey(c)){
                //这个字符是我们需要的
                window.put(c,window.getOrDefault(c,0)+1);
                if (need.get(c).equals(window.get(c))){
                    valid++;
                }
            }
            while (right-left==p.length()){
                //当前窗口的大小等于了我们要找的字符串
                if (valid==need.size()){
                    //因为我们这里要的是一个定长的结果 只能是abc bac这种
                    //所以满足条件不一定可以,比如abcd就不行
                    //所以在这个外面加了一个判断是当前窗口大小==需要字符串的长度
                    res.add(left);
                }
                char d=s.charAt(left);//我们要减去的窗口的字符
                left++;
                //进行缩减窗口
                if (need.containsKey(d)){
                    //减去的这个元素是我们需要的
                    if (window.get(d).equals(need.get(d))){
                        //当前元素的个数刚好是我们需要的,减去一个就不够了
                        valid--;
                    }
                    window.put(d,window.get(d)-1);
                }
            }
        }
        return res;
    }

}
  • 由于这道题中 [left, right) 其实维护的是一个定长的窗口,窗口大小为 t.size()。因为定长窗口每次向前滑动时只会移出一个字符,所以可以把内层的 while 改成 if,效果是一样的

    public int lengthOfLongestSubstring(String s) {
        HashMap<Character,Integer> window=new HashMap<>();
        int left=0;
        int right=0;
        int max=0;
        while (right<s.length()){
            char c=s.charAt(right);//我们要更新的字符
            right++;
            //窗口变大
            //--------------------------------------------
            window.put(c,window.getOrDefault(c,0)+1);//将这个数据放进去 数据处理
            //--------------------------------------------
            while (window.get(c)>1){
                //说明当前字符串不满足了要求了
                //窗口要减小
                char d=s.charAt(left);
                left++;
                //-----------------------
                window.put(d,window.get(d)-1);//数据处理
            }
            //----------------------------------------------
            max=Math.max(right-left,max);
        }
        return max;
    }

239滑动窗口的最大值

  •  这道题,我们首先想到用优先级队列(堆)来解决问题,因为堆这种结构最大的功能就是k个元素中找到最大值,但是有一个致命的缺陷,我们窗口移动的时候,如果最左边的元素的这些里面最大的值(比如元素的 5 2 1 4)从5 2 1 到2 1 4,其中最大值应该是4,但是我们最移动的时候(堆中的元素从 5 2 1 变成 5 2 1 4),但是5并不是我们范围内的元素,但是堆没有这种功能,来判断是不是范围内元素,所以我们传入堆中的数据,传一个键值对,值和对应的索引
class Solution {
         public int[] maxSlidingWindow(int[] nums, int k) {
        PriorityQueue<int[]> window=new PriorityQueue<>(new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0]!=o2[0]?o2[0]-o1[0]:o2[1]-o1[1];
            }
        });
        //将堆中先放入k个元素
        for (int i = 0; i < k; i++) {
            window.offer(new int[]{nums[i],i});
        }
        int []res=new int[nums.length-k+1];
        res[0]=window.peek()[0];//取出当前堆中的最大元素
        for (int i = k; i <nums.length; i++) {
            window.offer(new int[]{nums[i],i});
            while (window.peek()[1]<i-k+1){
                window.poll();
                //移除最大元素,但是不属于当前窗口的
            }
            res[i-k+1]= window.peek()[0];
        }
        return res;
    }



}

利用单调队列来实现

class Solution {
    class MonotonicQueue {
        LinkedList<Integer> dq = new LinkedList<>();
        public void push(int n) {
            while (!dq.isEmpty() && n > dq.getLast()) {
                dq.pollLast();
            }
            dq.addLast(n);
            //通过删除一起比要添加数字要小的元素,形成一个递减的队列
        }

        public int max() {
            return dq.getFirst();
            //第一个元素是最大值
        }

        public void pop(int n) {
            if (n == dq.getFirst()) {
                dq.pollFirst();
            }
        }
    }
    public int[] maxSlidingWindow(int[] nums, int k) {
        MonotonicQueue window=new MonotonicQueue();
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            if (i<k-1){
                window.push(nums[i]);
            }else {
                //窗口向前滑动一个大小
                window.push(nums[i]);
                res.add(window.max());
                window.pop(nums[i-k+1]);
            }
        }
        // 需要转成 int[] 数组再返回
        int[] arr = new int[res.size()];
        for (int i = 0; i < res.size(); i++) {
            arr[i] = res.get(i);
        }
        return arr;
    }
}
  •  首先利用队列这个结构,先进先出(我们可以实现每次让最先进来出去),为什么要变成单调的队列呢?因为我们要的是最大值,我们弄一个递减的队列,每次头都是最大值,我们去判断,如果队列的头的值是我们刚刚窗口最左边的值,我们可以将这个值删除,就不会影响我们向右移动,窗口左边的值影响我们取最大值

入队操作push

  •  每次先将前面那些小于要添加的元素的值删除,这样就可以得到一个递增的队列

关于删除,因为只能从头去删除,对于我们这道题,我们什么时候去删除队列的头元素呢

  •  我们知道,我们去移动窗口的时候,如果窗口最左边的数字等于我们当前窗口的最大值(单调队列的头节点),那么我们应该去删除头节点的值,如果最左边的数不是我们的最大值,就不需要去删除

剑指 Offer 57 - II. 和为s的连续正数序列

class Solution {
    public int[][] findContinuousSequence(int target) {
            int left=1;
            int right=1;
            int sum=0;
            List<int[]> res = new ArrayList<>();
            while(right<target){
                int i=right;
                right++;
                sum+=i;
                while(sum>=target){
                    if(sum==target){
                        int arr[]=new int [right-left];
                        for(int j=left;j<right;j++){
                             arr[j-left]=j;
                        }
                        res.add(arr);
                    }
                    int i1=left;
                    sum-=i1;
                    left++;
                }
            }
        return res.toArray(new int[res.size()][]);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库里不会投三分

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值