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

文章目录

  • 前言
  • 最长字串专场
    • 无重复字符的最长字串
    • 至多包含两个不同字串的最长子串
    • 至多包含K个不同字串的最长子串
  • 长度最小的子数组
  • 盛水最多的容器
  • 寻找字串异位词(排序)
    • 字符串的排序
    • 找到字符串中所有字母异位
  • 总结


前言


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

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

最长字串专场

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

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

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

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

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

无重复字符的最长字串

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

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

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

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

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

算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解_第3张图片
如果已经出现过,就和上图的做法一致,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);

完整代码如下:

/**
     * 最长无重复字串
     * @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;

    }

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

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

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

你需要考虑两个问题:

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

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

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

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

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

还是看图更直接一些:
我们可以充分利用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;
    }

至多包含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;
    }

长度最小的子数组

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

算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解_第4张图片
算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解_第5张图片
本题目可以使用双指针来解决,可以视为队列法。基本思路是先让元素不断入队,当入队元素和等于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;
    }

盛水最多的容器

参考地址:11. 盛最多水的容器 - 力扣(LeetCode)
算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解_第6张图片
这个题目看似很复杂,但是换个思路很简单的,假设两个指针i,j。指向的水槽板高度分别为h[i],h[j],此时状态下水槽面积为s(i,j)。由于可容纳水的高度有两个板中的腐短板决定的,因此可以得出面积公式:

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

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

每个状态,不论是长板还是短板向中间收缩一格,都会导致水槽边宽度-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;
    }

寻找字串异位词(排序)

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

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

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

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

字符串的排序

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

算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解_第7张图片
本题最简单的方式就是用s1做窗口,依次比较得出结果,但是这样的效率太低,我们看看有没有更好更优的办法。

所谓异位词要抓住两点:

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

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

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

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

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

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

而left向右移动就执行

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

完整代码是下面的:

  /**
     * 字符串的排列
     * @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;
    }

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

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

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

算法通过村第十六关-滑动窗口|白银笔记|经典题目讲解_第8张图片
思路可以说是一摸一样,简单改下代码就可以了,唯一不同的是,采用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;
    }

总结

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


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

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

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

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

你可能感兴趣的:(算法集训营,算法笔记,滑动窗口,双指针变种,字符串异位词(排序),最长子串问题,Java)