算法分享系列--滑动窗口问题

1、滑动窗口问题

        滑动窗口是【双指针】问题的一个经典场景,双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。

        若两个指针指向同一数组, 遍历方向相同且不会相交 ,则也称为 滑动窗口(两个指针包围的
区域即为当前的窗口) ,经常用于 区间搜索
        若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是
排好序的

2、滑动窗口窗口问题LeetCode举例

LeetCode中滑动窗口相关题目:

力扣https://leetcode.cn/problems/longest-substring-without-repeating-characters/solution/hua-dong-chuang-kou-by-powcai/

2-1、LeetCode-76-最小覆盖子串(困难)

题目如下:

算法分享系列--滑动窗口问题_第1张图片

解题思路如下:

        本题是滑动窗口的经典案例,用 i,j 表示滑动窗口的左边界和右边界,通过改变 i,j 来扩展和收缩滑动窗口,可以想象成一个窗口在字符串上游走,当这个窗口包含的元素满足条件,即包含字符串T的所有元素,记录下这个滑动窗口的长度 j-i+1这些长度中的最小值就是要求的结果。

步骤1: 不断的增加 j, 使滑动窗口增大,直到窗口包含了T的全部元素

步骤2: 不断的增加i, 缩小滑动窗口,将不必要的元素排除,包含T的全部元素的最小长度

步骤3: 此时i+1, 必然不满足条件了,从新步骤1,寻找新的满足条件的滑动窗口的最小值,直到最后j 遍历到字符串的末尾。

在此特别推荐大佬--labuladong写的滑动窗口类问题分析笔记,链接如下:

力扣https://leetcode.cn/problems/find-all-anagrams-in-a-string/solution/hua-dong-chuang-kou-tong-yong-si-xiang-jie-jue-zi-/本题的解析思路既:

滑动窗口算法的思路是这样:

  • 1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
  • 2、我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
  • 3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
  • 4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

现在开始套模板,只需要思考以下四个问题

  • 1、当移动 right 扩大窗口,即加入字符时,应该更新哪些数据?
  • 2、什么条件下,窗口应该暂停扩大,开始移动 left 缩小窗口?
  • 3、当移动 left 缩小窗口,即移出字符时,应该更新哪些数据?
  • 4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

实现代码如下【JAVA实现版本】

class Solution {
    public String minWindow(String s, String t) {
         Map need = new HashMap();
         Map window = new HashMap();
         for(char c : t.toCharArray()){
            if(need.containsKey(c)){
                need.put(c, need.get(c) + 1);
            }else{
                need.put(c, 1);
            }
         }

         int left = 0; int right = 0;
         int valid = 0;
         //记录最小覆盖子串的起始位置,以及长度
         int start = 0; int len = Integer.MAX_VALUE;

         boolean flag = false;
         while(right < s.length()){
             //获取s的右边字符
             char c = s.charAt(right);
             //右移窗口
             right++;
             //如果这个字符,也是t字符的组成字符之一,则窗口内进行一系列更新
             if(need.containsKey(c)){
                if(window.containsKey(c)){
                   window.put(c, window.get(c) + 1);
                }else{
                   window.put(c, 1);
                }
                //两个map的同一个字符满足相同个数时,证明一个字母已经无需再获取了
                if(window.get(c).equals(need.get(c))){
                   valid++;
                }
             }

             //获取有效字母数量,跟需要获取的一致化,则说明都包含了
             //while循环,保障在满足条件的前提下,滑动窗口逐步递减,获取最小子串
             while(valid == need.size()){
                 flag = true;
                 //在这里更新最小覆盖子串
                 if(right - left < len){
                     len = right - left;
                     start = left;
                 }
                 //获取【滑动窗口】的最左边字符
                 char d = s.charAt(left);
                 //此时可以尝试减小左窗口
                 left++;
                 //如果移除的字符,也是t字符的组成字符之一,则窗口内进行一系列更新
                 if(need.containsKey(d)){
                     if(window.get(d).equals(need.get(d))){
                        valid--;
                     }
                     window.put(d, window.get(d) - 1);             
                 }
             }
         }
         return  !flag ? "" : s.substring(start, start+len);
    }
}

复杂度分析

时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历一遍,哈希表中对 s 中的每个元素各插入、删除一次,对 t 中的元素各插入一次。每次检查是否可行会遍历整个 t 的哈希表,哈希表的大小与字符集的大小有关,则渐进时间复杂度为 O(∣s∣+∣t∣)。
空间复杂度:这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小都不会超过字符串本身 ,则渐进空间复杂度为 O(∣s∣+∣t∣)。

解题特点

       比较字符出现次数时,window.get(d).equals(need.get(d))  ,之前写成           window.get(d).== (need.get(d)) 对于长字符就会出错,Integer 在大于127 的时候是对象,因此不能使用数字模式进行比较了!

2-2、LeetCode-3-无重复字符的最长子串(中等)

题目如下:

算法分享系列--滑动窗口问题_第2张图片

解题思路如下:【滑动窗口逻辑】,其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!       

方法一:借助队列容器

class Solution {
    public int lengthOfLongestSubstring(String s) {
        if(s==null || s.length() == 0) return 0;
        //按照顺序FIFO模式
        Queue queue  = new LinkedList();
        int res = 0;
        for(int i =0; i< s.length(); i++){
            char c= s.charAt(i);
            while(queue.contains(c)){
                queue.poll();
            }
            queue.offer(c);
            res = Math.max(res, queue.size());
        }
        return res;
    }
}

时间:O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。

空间:O(N),及辅助空间队列的大小。

方法二:使用哈希表记录是否重复【滑动窗口思路】

class Solution {
    public int lengthOfLongestSubstring(String s) {
        char[] chars = s.toCharArray();
        Map map = new HashMap<>();
        int res = 0;
        for(int start = 0, end = 0; end < s.length(); end++ ){
            char c = chars[end];
            if(map.containsKey(c)){
                 start = Math.max( map.get(c), start);
            }
            res = Math.max(res, end - start + 1);
            map.put(c, end+1);
        }
        return res;
    }
}

时间:O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。

空间:O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0, 128) 内的字符,即 ∣Σ∣=128。需要用到哈希集合来存储出现过的字符,而字符最多有 |Σ∣ 个,因此空间复杂度为O(∣Σ∣)。

你可能感兴趣的:(程序员刷题分析成长之路,java,面试,算法,leetcode,个人开发)