数组题目总结 -- 滑动窗口算法

零. 模板

    public static void slidingWindow(String str){
        //用合适的数据结构记录窗口的数据
        HashMap<Character, Integer> window = new HashMap<>();

        int left = 0, right = 0;
        while(right < str.length()){
            //将c移入窗口
            char c = str.charAt(right);
            window.put(c, window.getOrDefault(c, 0) + 1);[添加链接描述](https://leetcode.cn/problems/minimum-window-substring/)
            //增大窗口
            right++;
            //进行窗口数据的一些列更新......这里以输出left right为例子,可删除
            System.out.println("left" + left + "_"+ "right:" + right);
        }
        //判断左窗口是否需要收缩
        while(left < right && window needs shrink){
            //d是移出窗口的字符
            char d = str.charAt(left);
            window.put(d, window.getOrDefault(left, 0) - 1);
            //缩小窗口
            left++;
            //进行窗口数据的更新......
        }
    }
  • 时间复杂度:O(n),n为字符串/数组的长度

一. 最小覆盖子串

思路和代码:

I. 博主的做法

不会,,太难了

II. 东哥的做法

  • 第零步:构建两个map

    • needs:用来记录给定子串 t 中的字符及存在的个数
    • window:用来记录窗口 window 中的字符及存在的个数
      在这里插入图片描述
  • 第一步:遍历 字符串 t,将里面的字符及个数存入needs里

  • 第二步:将窗口向右扩展,如果这个字符在 needs 中,更新window 和 valid,直到 window 包含了 t 中所有的字符
    在这里插入图片描述

  • 第三步:检查 valid 与 needs 大小是否相等,如果相等,说明我们的窗口已经满足条件,可以左边缘右移

  • 第四步:如果子串长度小于上一个子串长度 -> 更新子串(子串信息由 start 和 len 来存储)

  • 第五步:窗口左边缘右移,更新window,如果window[c_out] 内的出现次数和 need[c_out] 相等,更新 valid
    在这里插入图片描述

    • 只有当 window[c_out] 内的出现次数和 need[c_out] 相等时,才能 -1(左移0)
      • 如果 window[c_out] < need[c_out],第二个while循环根本进不来,不可能
      • 如果 window[c_out] > need[c_out],那么就算c_out移出去也无所谓,依然满足window包含 t 中所有字符
    • 即使左移不满足要求了,也没关系,left刚好指的是新字符串的起始位置,寻找新的子串
      在这里插入图片描述
  • 第六步: 一直重复第二步 ~ 第四步,直到 right 最终来到了字符串 s 的尾部

class Solution {
    public String minWindow(String s, String t) {
    	//第零步
        Map<Character, Integer> needs = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();

        int left = 0, right = 0;
        int valid = 0;
        int start = 0, len = Integer.MAX_VALUE;
        //第一步
        for(char c : t.toCharArray())
            needs.put(c, needs.getOrDefault(c,0) + 1);
		//第六步(重复)
        while(right < s.length()){
        	//第二步
            char c_in = s.charAt(right);
            
            right++;
            //第三步
            if(needs.containsKey(c_in)){
                window.put(c_in, window.getOrDefault(c_in, 0)+1);
                if(window.get(c_in).equals(needs.get(c_in)))
                    valid++;
            }
			
            while(valid == needs.size()){
            	//第四步
                if(right - left < len){
                    start = left;
                    len = right - left;
                }
                //第五步
                char c_out = s.charAt(left);
                
                left++;
                
                if(needs.containsKey(c_out)){
                	//如果刚刚好,那么才能减一。如果window多于needs,那么移出去也不影响
                	//当然,如果window < needs,那这个while循环都进不来
                    if(window.get(c_out).equals(needs.get(c_out)))
                        valid--;
                    //如果valid减小了,也无所谓,left刚好指的是新字符串的起始位置,寻找新的子串
                    window.put(c_out, window.get(c_out)-1);
                }
                

            }

        }
        return len == Integer.MAX_VALUE ? "" : s.substring(start, start+len);

    }
}
  • 时间复杂度:O(|S| + |T|),|S| 表示字符串 s 的长度,|T| 表示字符串 t 的长度
  • 空间复杂度:O(|2T|),这里使用了两个 hashmap 来存储数据,每一个的长度不会超过 T 的长度
    • 如果有重复字母,needs.size() 就比 t.length() 小

二. 字符串的排列

思路和代码:

I. 博主的做法

博主本来想用给的 s1 子串,算出所有的全排列再放到滑动窗口里。仔细想想太麻烦了。

II. 东哥的做法

  • 和上个题非常的像,只是在左边缘右移的时候有些变化
  • 所谓全排列,其实就是:串的长度一样并且里面各个字符的个数也相同。
  • 第零步:构建needs,window两个hashmap
  • 第一步:将 s1 的值放入 needs 中
  • 第二步:窗口右移
  • 第三步:如果窗口的长度等于 s1 的长度了
    • 如果 valid 和 needs 的大小相同,也就是找到了子串,返回true
    • 如果不同,那么窗口左边缘右移
  • 第四步:重复第二 ~ 四步,直到 right 来到了 s2 串的尾部
  • 这里需要注意的是:里面的 while 可以换成 if ,因为这个题是固定窗口大小的(s1的长度),最多滑动也只是 1 个元素
  • 这两句的顺序一定不能换:一点是先判断是否满足需求,再进行 -1 操作!!!!!!!
        if(needs.get(c_out).equals(window.get(c_out)))
            valid--;
        window.put(c_out, window.get(c_out) - 1);
class Solution {
    public boolean checkInclusion(String s1, String s2) {
    	//第零步
        Map<Character, Integer> needs = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();
		//第一步
        for(char c : s1.toCharArray())
            needs.put(c, needs.getOrDefault(c,0)+1);
        
        int left = 0, right = 0;
        int valid = 0;
		//第四步(重复)
        while(right < s2.length()){
        	//第二步
            char c_in = s2.charAt(right);

            right++;

            if(needs.containsKey(c_in)){
                window.put(c_in, window.getOrDefault(c_in, 0) + 1);
                if(window.get(c_in).equals(needs.get(c_in)))
                    valid++;
            }
			//写成这样也可以:if(right - left == s1.length()){
			//窗口扩大到 s1 的长度就可以了
            while(right - left == s1.length()){
            	//第三步
                if(valid == needs.size())
                    return true;
                char c_out = s2.charAt(left);
                left++;
                if(needs.containsKey(c_out)){                    
                    if(needs.get(c_out).equals(window.get(c_out)))
                        valid--;
                    window.put(c_out, window.get(c_out) - 1);
                }
            }

        }
        return false;
    }
}
  • 时间复杂度:O(|S| + |T|),|S| 表示字符串 s2 的长度,|T| 表示字符串 s1 的长度
  • 空间复杂度:O(|2T|),这里使用了两个 hashmap 来存储数据,每一个的长度不会超过 T 的长度

三. 找到字符串中所有字母异位词

思路和代码:

  • 和上道题基本上一模一样,只不过这次要返回每一个子串的首地址
    • 我们申请一个ArrayList,每次找到,将首地址加进去就好了
  • 需要注意的是:needs.size() 不一定等于 p.length()
    • 如果有重复字母,那么needs.size() 只算一次
      • eg:String p = “11111”;
      • needs.size() = 1; p.length() = 5;
    • 在子串判断的时候一定要使用 needs.size() == valid,而不是使用 p.length()

I. 博主的做法

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        Map<Character, Integer> needs = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();
        List<Integer> result = new ArrayList<>();


        int left = 0, right = 0;
        int valid = 0;


        for(char c : p.toCharArray())
            needs.put(c, needs.getOrDefault(c, 0) + 1);



        while(right < s.length()){
            char c_in = s.charAt(right);
            right++;

            if(needs.containsKey(c_in)){
                window.put(c_in, window.getOrDefault(c_in, 0) + 1);
                if(window.get(c_in).equals(needs.get(c_in)))
                    valid++;
            }

            while(right - left == p.length()){
                if(valid == needs.size())
                    result.add(left);
                char c_out = s.charAt(left);
                left++;
                if(needs.containsKey(c_out)){
                    if(needs.get(c_out).equals(window.get(c_out)))
                        valid--;
                    window.put(c_out, window.get(c_out) - 1);
                }
            }

            
        }
        return result;
    }
}

II. 东哥的做法

  • 和博主做的一样
  • 时间复杂度:O(|S| + |T|),|S| 表示字符串 s 的长度,|T| 表示字符串 p 的长度
  • 空间复杂度:O(|2T|),这里使用了两个 hashmap 来存储数据,每一个的长度不会超过 T 的长度

四. 最长无重复子串

思路和代码:

I. 博主的做法

  • 博主这里,将map换成了ArrayList,其实是一样的,换成队列也可以
  • 第一步:如果 window 里没有c_in,那我们就加入,窗口右移
  • 第二步:if 是针对不同字母的
    • 如果都是不同字母,那窗口一直右移动就可以,不断更新 max
    • 多写了一遍:max = max < len ? len : max; 是防止 s 都是不同的字母,根本不经过 else
  • 第三步:else 是针对有重复字母的
    • 如果有重复字母(right 指向的是重复字母),那我们让 right 回退一格,始终不让它前进,left 右移,缩小窗口(这里处理的是eg:abb;这种重复,最终 left 和 right 重合,window 里只剩下 b)
class Solution {
    public int lengthOfLongestSubstring(String s) {
        int left = 0, right = 0;
        int len = 0;
        int max = 0;

        List<Character> window = new ArrayList<>();
        
        while(right < s.length()){
            char c_in = s.charAt(right);
            right++;

            if(!window.contains(c_in)){
                window.add(c_in);
                len++;
                max = max < len ? len : max;
            }
            else{
                max = max < len ? len : max;
                char c_out = s.charAt(left);
                window.remove((Object)c_out);

                left++;
                right--;
                len--;
            }
            
        }
        return max;
    }
}
  • 时间复杂度:O(|S| ),|S| 表示字符串 s 的长度
  • 空间复杂度:O(|S|),这里使用了一个 hashmap 来存储数据,最长不会超过 s 的长度

II. 东哥的做法

  • 第一步:不管是啥,先窗口右移
  • 第二步:如果当前的 c 多于一个,不停的缩小窗口,直到 c 最后是一个
  • 第三步:不停的更新 res
int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> window = new HashMap<>();

    int left = 0, right = 0;
    int res = 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);
        }
        // 在这里更新答案
        res = Math.max(res, right - left);
    }
    return res;
}

  • 时间复杂度:O(|S| ),|S| 表示字符串 s 的长度
  • 空间复杂度:O(|S|),这里使用了一个 hashmap 来存储数据,最长不会超过 s 的长度

参考:https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/wo-xie-le–f7a92/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值