• 算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解



    前言


    提示:所有的话语都颇为类似,而沉默则千差万别。 --卢梭

    趁热打铁,我们继续来研究一些高频、热门的滑动窗口问题。

    最长字串专场

    先看最高频的推荐题目⭐⭐⭐⭐:

    3. 无重复字符的最长子串 - 力扣(LeetCode)

    滑动窗口—至多包含两个不同字符的最长子串(leetcode 159)_珠穆拉玛峰的博客-CSDN博客

    LeetCode算法日记:340.至多包含K个不同字符的最长子串_算法 至多-CSDN博客

    看这题目,我们要怎么处理呢?这种造题是不是自己拿手的,顺着这个思路是不是可以造出很多题目来,总结出一些方法,解滑动窗口的模板呢?

    无重复字符的最长字串

    参考题目地址:3. 无重复字符的最长子串 - 力扣(LeetCode)

    在这里插入图片描述

    在这里插入图片描述

    要找到最长的子串,必然要知道无重复字符串的首部和尾部,然后再从中确定最长的那个,因此至少需要两个指针才可以,这也是滑动窗口思想。即使用滑动窗口,深入分析会发现,具体处理来由很多方式。这里介绍一种经典的使用Map的思考。

    我们这里定义一个K—V形式的map,key表示的是当前正在访问的字符串,value是器下标索引值。我们每访问一个新元素,都将其下标更新成对应的索引值。

    在这里插入图片描述
    如果已经出现过,就和上图的做法一致,abcabc,当第二次出现a的时候,我们就更新left成为第一个b的所在位置,此时惊奇的事情发生了,left要移动的位置恰好就是map.get(‘a’)+1= 1,我们将‘a’用序列来表示,放在一起就是map.get(s.charAt(i)) + 1。其他情况可以类似考虑,参考上图见解。

    当然这里有一种特殊的情况,例如abba,我们再次访问b时,left = map.get(‘b’) + 1 = 2。然后继续访问第二个a,这时 left = map.get(‘a’) + 1 = 1,也就是left要后推的节奏,显然就不对了。所以这里要调整逻辑,保留最大值。

    这样就可以了

    left = Math.max(left,map.get(s.charAt(i)) + 1);
    
    • 1

    完整代码如下:

    /**
         * 最长无重复字串
         * @param s
         * @return
         */
        public static int lengthOfLongestSubstring(String s) {
           if (s == null || s.length() == 0){
               return 0;
           }
            Map<Character,Integer> map = new HashMap<>();
           int max = 0;
           int left = 0;
           for(int right = 0; right < s.length(); right++){
               if (map.containsKey(s.charAt(right))){
                   // 确保left一直向前走 不回头
                   left = Math.max(left,map.get(s.charAt(right)) + 1);
               }
               map.put(s.charAt(right),right);
               max = Math.max(max,right - left + 1);
           }
           return max;
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    除了上述方法,不用Hash存储索引,也是可以用滑动窗口的思想来解决的,感兴趣的是试一试。

    至多包含两个不同字串的最长子串

    根据上题目的变种,试想以下,这里如果求包含两个不同字串的最长子串呢。

    你需要考虑两个问题:

    • 怎么判断是两个
    • 什么删除元素

    如果还使用上述方法是否行的通,left的值应该怎么求舍?

    要判断只有两个元素,当然还是Hash好用一些,每一个时刻,判断HashMap中不得超过3个元素。这里就要解决下一个问题,怎么移除了。

    还是采用key-value的含义,这次将字符串里的字符都当做键,在窗口中的最右边的字符位置作为值。我们在看下伪代码是删除的逻辑:

    del_index = Collections.min(hashMap.values());
    left = del_index + 1;
    
    • 1
    • 2

    还是看图更直接一些:
    我们可以充分利用Map来解决这个问题:

     /**
         * 至少包含两个字符的最长子串
         * @param s
         * @return
         */
        public static int lengthOfLongestSubstringTwoDistinct(String s) {
            if (s.length() < 3){
                return s.length();
            }
    
            int left = 0;
            int right = 0;
            HashMap<Character,Integer> map = new HashMap<>();
            int maxLen = 2;
            while(left < s.length()){
                if (map.size() < 3){
                    map.put(s.charAt(right),right++);
                }
                // 如果大于3就靠考虑了
                if (map.size() == 3){
                    // 删除最左侧的位置
                    int del_idx = Collections.min(map.values());
                    map.remove(s.charAt(del_idx));
                    // 更新窗口的位置
                    left = del_idx + 1;
                }
                maxLen = Math.max(maxLen, right - left);
            }
            return maxLen;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    至多包含K个不同字串的最长子串

    这更改2的位置就可以了,正如上个题目一样:

    只要将判断HashMap大小为2改成k就可以了,超过2就是k+ 1,实现不就很简单的事情。

        /**
         * 至多包含k个字符的最长子串
         *
         * @param s
         * @param k
         * @return
         */
        public static int lengthOfLongestSubstringKDistinct(String s, int k) {
            if (s.length() < k + 1) {
                return s.length();
            }
    
            int left = 0, right = 0;
            HashMap hashmap = new HashMap<>();
            int maxLen = k;
    
            while (right < s.length()) {
    
                if (hashmap.size() < k + 1) {
                    hashmap.put(s.charAt(right), right++);
                }
                // 如果大小达到了k个
                if (hashmap.size() == k + 1) {
                    //
                    int del_idx = Collections.min(hashmap.values());
                    hashmap.remove(s.charAt(del_idx));
                    // 窗口left的新位置
                    left = del_idx + 1;
                }
    
                maxLen = Math.max(maxLen, right - left);
            }
            return maxLen;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    长度最小的子数组

    参考题目介绍:209. 长度最小的子数组 - 力扣(LeetCode)

    在这里插入图片描述
    在这里插入图片描述
    本题目可以使用双指针来解决,可以视为队列法。基本思路是先让元素不断入队,当入队元素和等于target时就记录以下此时队列的容量,如果队列元素之和大于target则开始出队,知道小于target则再入队。

    当然如果等于target的情况,则记录以下此时队列的大小,之后继续先入队再出队。每当出现元素之和等于target时我们就保留容量最小的那个。

     /**
         * 长度最小的子数组
         * @param target 目标值
         * @param nums  数组
         * @return 返回大小
         */
        public static int minSubArrayLen(int target, int[] nums) {
    
            // 初始化变量
            int left = 0, right = 0,sum = 0,min = Integer.MAX_VALUE;
            while(right < nums.length){
                sum += nums[right++];
                while(sum >= target){
                    min = Math.min(min,right - left);
                    sum -= nums[left++];
                }
            }
            return min == Integer.MAX_VALUE ? 0 : min;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    盛水最多的容器

    参考地址:11. 盛最多水的容器 - 力扣(LeetCode)
    在这里插入图片描述
    这个题目看似很复杂,但是换个思路很简单的,假设两个指针i,j。指向的水槽板高度分别为h[i],h[j],此时状态下水槽面积为s(i,j)。由于可容纳水的高度有两个板中的腐短板决定的,因此可以得出面积公式:

    s(i,j) = min(h[i],h[j])*(j - i)
    
    • 1

    那么我们就要考虑以下问题:

    每个状态,不论是长板还是短板向中间收缩一格,都会导致水槽边宽度-1变短:

    • 若向内移动短板,水槽得短板min(h[i],h[j])可能变大,因此下一个水槽得面积可能会增加
    • 若向内移动长板,水槽得短板min(h[i],h[j])不变或者变小,因此下一个水槽得面积一定是变小的

    因此,这里初始化双指针分别位于水槽左右两端,循环每轮移动向内移动一格,并更新面积最大值,知道两个指针相遇时跳出;即可获得最大面积。

    	/**
         * 盛最多水的容器
         * @param height the height of the array
         * @return
         */
        public static int maxArea(int[] height) {
            int i = 0, j = height.length,res = 0;
            while(i < j){
                // 那最小向中间移动
                res = height[i] < height[j] ?
                        Math.max(res,(j - i)* height[i++]):
                        Math.max(res,(j - i)* height[j++]);
            }
            return res;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    寻找字串异位词(排序)

    如果两个字符串仅仅时字母出现的位置不一样,则称两者互相为对方的一个排列,也常称为异位词。

    这里推荐两个题目⭐⭐⭐⭐:

    567. 字符串的排列 - 力扣(LeetCode)

    438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

    字符串的排序

    参考题目地址:567. 字符串的排列 - 力扣(LeetCode)

    在这里插入图片描述
    本题最简单的方式就是用s1做窗口,依次比较得出结果,但是这样的效率太低,我们看看有没有更好更优的办法。

    所谓异位词要抓住两点:

    • 字母类型一样
    • 出现个数相同

    因此,我们可以创建一个为26的数组,每个位置对应a-z的个数,为了方便操作,索引做了调整,

    index = s1.charAt(i) - 'a';
    
    • 1

    这个是字符串常用的技巧。

    此时窗口一直向前移动就行了。

    charArray[s2.charAt(right) - 'a']++
    
    • 1

    而left向右移动就执行

    int left = right - sLen1;
    charArray2[s2.charAt(left) - 'a']--;
    
    • 1
    • 2

    完整代码是下面的:

      /**
         * 字符串的排列
         * @param s1
         * @param s2
         * @return
         */
        public static boolean checkInclusion(String s1, String s2) {
            // 参数检验
          int len1 = s1.length(),len2 = s2.length();
          if (len1 > len2){
              return false;
          }
    
            int[] chars1 = new int[26];
            int[] chars2 = new int[26];
            // 先读len1
            for (int i = 0; i < len1; i++){
                chars1[s1.charAt(i) - 'a']++;
                chars2[s1.charAt(i) - 'a']++;
            }
    
            if (Arrays.equals(chars1, chars2)){
                return true;
            }
            // 注意维护窗口变化
            for (int right = len1; right < len2; right++){
                chars2[s2.charAt(right) - 'a']++;
                int left = right - len1;
                chars2[s2.charAt(left) - 'a']--;
                if (Arrays.equals(chars1, chars2)){
                    return true;
                }
            }
            return false;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    上面只是判断有没有,那么如果让你确定右几个呢?有或者如果存在的话,将异位词的开始位置和结束位置输出出来怎么样?那就看看下一题吧

    找到字符串中所有字母异位

    参考题目介绍:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

    在这里插入图片描述
    思路可以说是一摸一样,简单改下代码就可以了,唯一不同的是,采用List存放,出现异位词的,记录以下开始位置,那么可以直接将其加入到list中。

    开下完整的代码:

        /**
         * 找到字符串中所有字母异位词
         * @param s1
         * @param s2
         * @return
         */
        public static List<Integer> findAnagrams(String s1, String s2) {
            // 参数检验
            int len1 = s1.length(),len2 = s2.length();
            if (len1 > len2){
                return new ArrayList<Integer>();
            }
    
            int[] chars1 = new int[26];
            int[] chars2 = new int[26];
            // 先读len1
            for (int i = 0; i < len1; i++){
                chars1[s1.charAt(i) - 'a']++;
                chars2[s1.charAt(i) - 'a']++;
            }
            ArrayList<Integer> res = new ArrayList<>();
    
            if (Arrays.equals(chars1, chars2)){
                res.add(0);
            }
            // 注意维护窗口变化
            for (int left = 0; left < len1 - len2; left++ ){
                chars2[s1.charAt(left) - 'a']--;
                int right = left + len1;
                chars2[s1.charAt(right) - 'a']++;
                if (Arrays.equals(chars1, chars2)){
                	// 拿到最最左位置
                    res.add(left + 1);
                }
            }
            return res;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    总结

    提示:滑动窗口问题;双指针问题;字符串变种;HashMap的特殊存储;模板套路


    如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ ("▔□▔)/

    如有不理解的地方,欢迎你在评论区给我留言,我都会逐一回复 ~

    也欢迎你 关注我 ,喜欢交朋友,喜欢一起探讨问题。

    在这里插入图片描述

  • 相关阅读:
    VMware16安装CentOS 7.9操作系统(Minimal版)
    使用VSCode新建解决方案,添加ClassLib类库工程
    禾观科技采用亚马逊云科技的数据湖,实现数据化驱动运营的核心
    Object.prototype.toString.call()如何理解
    SpringBoot 2 配置文件 2.5 配置文件分类
    【Mysql】复合查询
    【JAVASE】带你了解面向对象三大特性之一(继承)
    @GetMapping、@PostMapping 和 @RequestMapping详细区别附实战代码(全)
    TreeUtils工具类一行代码实现列表转树【第三版优化】 三级菜单 三级分类 附视频
    CentOS6/7 配置守护进程
  • 原文地址:https://blog.csdn.net/weixin_46585492/article/details/134036739