zoukankan      html  css  js  c++  java
  • 聊聊算法——滑动窗口

    有看到一句话,我深以为然:“所有算法的终极数据结构只有两种:数组和链表!”其他所有数据结构都是数组或链表的衍生品,

    不管是树还是图或者栈,至于算法就最终都落到了这两种结构的操作上,滑动窗口也不例外!滑动窗口的应用场景还是很多的:

    HTTP的帧传输,滑动窗口限流算法、Flink中的滑动窗口等,今天,我们就来聊聊滑动窗口的算法框架!

    作者原创文章,谢绝一切转载,违者必究!

    准备:

    Idea2019.03/JDK11.0.4

    难度: 新手--战士--老兵--大师

    目标:

    1. 滑动窗口算法分析与应用

    1 算法框架

    先给出滑动窗口算法框架:

    /* 滑动窗口算法框架 */
    private static void minWindow(String s,String t){
        // 滑动窗口左右侧位置指针
        int left = 0,right = 0;
        while (right < s.length()){
            // 滑动窗口右边增加
            right ++;
            if ( 滑动窗口满足目标){
                //对窗口内元素操作 
            }
            // 滑动窗口左边缩小
            while ( 滑动窗口缩小条件 ){
                // 
                left ++;
            }
        }
    }

    我们以实际例子来加强理解,来个 hard 级别的玩一下,力扣第76题:

    给你一个字符串 S、一个字符串 T,请在字符串 S 里面找出:包含 T 所有字符的最小子串:

    示例,输入: S = "ADOBECODEBANC", T = "ABC",输出: "BANC",如果不存在则返回空串。

    先直接写出解法如下(Java),略长,耐心看完必有收获:

    public class SlideWindow {
        public static void main(String[] args) {
            String s = "DBABECFCAB";
            String t = "ABC";
            System.out.println(minWindow(s,t));
        }
        private static String minWindow(String s,String t){
            if (s.length()==0 || t.length() ==0){
                return "NULL";
            }
            // 目标字符串使用Map存储,如果有重复的,则数量累加
            Map<Character,Integer> dictT = new HashMap<>(8);
            for (int i = 0; i < t.length(); i++) {
                int count = dictT.getOrDefault(t.charAt(i),0);
                dictT.put(t.charAt(i),count +1);
            }
            // 目标字符串的长度
            int required = dictT.size();
            // 滑动窗口左右侧位置指针
            int left = 0,right = 0;
            // 滑动窗口中字符串统计
            Map<Character,Integer> winStrMap = new HashMap<>(16);
            // 窗口内子串与目标串字符匹配的个数
            int formed = 0;
            // 记录最小窗口长度,{窗口长,left,right}
            int[] ans = {-1,0,0};
            //窗口进行滑动
            while (right < s.length()){
                char c = s.charAt(right);
                int count = winStrMap.getOrDefault(c,0);
                winStrMap.put(c,count + 1);
                // 匹配一个字符则记录一次
                if (dictT.containsKey(c) && winStrMap.get(c).intValue() == dictT.get(c).intValue()){
                    formed ++;
                }
                while (left < right && formed == required){
                    c = s.charAt(left);
                    //计算最小窗口长
                    if( ans[0] == -1 || (right - left + 1 < ans[0])){
                        ans[0] = right - left + 1;
                        ans[1] = left;
                        ans[2] = right;
                    }
                    winStrMap.put(c, winStrMap.get(c) -1);
                    if (dictT.containsKey(c) && winStrMap.get(c) < dictT.get(c)){
                        formed --;
                    }
                    // 进行左侧缩小滑动
                    left ++;
                }
                //窗口向右滑动
                right ++;
            }
            return ans[0] == -1 ? "NULL" : s.substring(ans[1],ans[2]+1);
        }
    }

    以上代码解释:使用两个Map分别记录目标字符串和滑动窗口内字符串的字符数量,并进行对比;在窗口向右滑动的过程中,

    如果满足目标条件则停下来进行窗口左侧缩小滑动,并记录下可行解的结果;窗口再向右滑动,并继续寻找可行解,直到右

    侧到达终点。 此题有改良思路,即先对搜索字符串中去掉不存在于目标字符串中的字符,这样滑动匹配次数就更少了。

    我做了一个动图,解释找到第一个可行解 {ABEC},既首次匹配到{A:1,B:1,C:1}的过程,注意此解不是最终解:

    2 应用秒杀

    既然明白了算法框架思路,来做秒杀,题1,力扣第 567 题Medium级别:

    给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。示例,输入: s1 = "ab",

    s2 = "eidbaooo",输出: True,解释: s2 包含 s1 的排列之一 ("ba").

    参考上面的算法,直接裁剪一下就出来了(Java):

    private static boolean subString(String s,String t){
        if (s.length()==0 || t.length() ==0){
            return false;
        }
        Map<Character,Integer> targetMap = new HashMap<>(8);
        for (int i = 0; i < t.length(); i++) {
            // 目标串中可能有重复字符,
            int count = targetMap.getOrDefault(t.charAt(i),0);
            targetMap.put(t.charAt(i),count + 1);
        }
        Map<Character,Integer> winMap = new HashMap<>(16);
        int left = 0,right = 0 ;
        int match = 0;
        while(right < s.length()){
            char c = s.charAt(right);
            int count = winMap.getOrDefault(c,0);
            winMap.put(c,count + 1);
            // 右侧一直向右滑动,直到包含了目标串的所有字符排列
            if (targetMap.containsKey(c) && targetMap.get(c).intValue() == winMap.get(c)){
                match ++;
            }
            right ++;
            // 窗口左侧向右滑动,判断满足所有字符排列的子串
            while (left < right && match == targetMap.size()){
                // 如果子串长度等于目标串长度,即为可行解
                if (right - left + 1 == targetMap.size()){
                    return true;
                }
                c = s.charAt(left);
                if (targetMap.containsKey(c) && targetMap.get(c).intValue() == winMap.get(c)){
                    count = winMap.getOrDefault(c,0);
                    winMap.put(c,count -1);
                    match --;
                }
                left ++;
            }
        }
        return false;
    }

    代码解析:在窗口扩大滑动过程中,先找到包含了目标串所有字符的子串,然后左侧滑动缩小,如果同时满足

    窗口中子串长度和目标串长度一样,因为既包含了所有字符且长度一样的子串肯定为一个排列,即为可行解!

     

    秒杀题2,力扣第3题 Medium级别:

    给定一个字符串,请你输出其中不含有重复字符的最长子串。示例: 输入, "ABEBCDEE", 输出:"EBCD",

    我这里并非原题,做了个变形,要求输出子串,而不是给出个长度值,解法如下:

    /**查找最长无重复子串*/
    private static String subString(String s) {
        if (s.length()==0){
            return "";
        }
        int left = 0,right = 0;
        Map<Character,Integer> winMap = new HashMap<>(8);
        int[] position={0,0,0};
        while (right < s.length()){
            char c = s.charAt(right);
            int count = winMap.getOrDefault(c,0);
            winMap.put(c, count + 1);
            if (right - left > position[0]){
                position[0]= right - left;
                position[1]= left;
                position[2]= right;
            }
            while (winMap.get(c) > 1){
                if (s.charAt(right) == s.charAt(left)){
                    count = winMap.get(c);
                    winMap.put(c,count - 1);
                }
                left ++;
            }
            right ++;
        }
        return s.substring(position[1],position[2]);
    }

    代码解析:如何找重复字符?即记录字符个数并累加,大于1的即存在重复。同样是滑动窗口,但注意使用一个

    数组记录可行解,然后对比其他可行解,并只保留最优解。注意,最长无重复子串不唯一,如"ABEBCDEE",

    可行解为"EBCD"或者"BCDE",故原题仅输出长度值而只有唯一解。另外,这里其实有个优化思路,每次窗口右

    侧发现有重复字符,窗口左侧滑动时,可以不必逐字符滑动,可以直接定位到重复字符的下一位即可。

     

    后记:总以为算法平时用的少,工作也不是算法岗,所以没必要去研究,可是遭到社会的毒打后,才知道算法是很重要的,共勉!

     

    全文完!


    我近期其他文章:

    只写原创,敬请关注 

  • 相关阅读:
    【Express系列】第3篇——接入mysql
    【Express系列】第2篇——主程序的改造
    【Express系列】第1篇——项目创建
    AngularJS内置指令
    node服务端搭建学习笔记
    生成ssh key
    webstorm的常用操作
    VSCode 常用插件
    php集成包
    composer安装特别慢的解决方案
  • 原文地址:https://www.cnblogs.com/xxbiao/p/12968579.html
Copyright © 2011-2022 走看看