【Java算法】滑动窗口 下

   🔥个人主页: 中草药

🔥专栏:【算法工作坊】算法实战揭秘


🦌一.水果成篮

题目链接:904.水果成篮

算法原理

算法原理是使用“滑动窗口”(Sliding Window)策略,结合哈希表(Map)来高效地统计窗口内不同水果的种类数量。以下是详细分析:

  1. 初始化:创建一个空的哈希表 map 用来存储每种水果的数量,初始化左右指针 leftright 为 0,同时初始化结果变量 ret 为 0,用于记录最大的水果段长度。

  2. 扩展右边界:右指针 right 逐渐向右移动,每移动一步,就在哈希表 map 中增加对应水果的数量。这代表尝试将新的水果加入当前的采摘窗口。

  3. 处理超过两种水果的情况:使用 while 循环检查哈希表 map 的大小是否大于 2,即窗口内是否含有超过两种水果。如果是,则需要收缩左边界,即从窗口中移除最左边的水果(即减少其在哈希表中的计数,并在计数为 0 时从哈希表中移除该水果),然后将左指针 left 向右移动一位,缩小窗口范围。

  4. 更新结果:每次右指针移动后,都检查当前窗口(right-left+1)的长度是否大于之前记录的最大长度 ret,如果是,则更新 ret 为当前窗口的长度。

  5. 遍历结束:当右指针遍历完整个数组后,ret 中存储的就是能够收集到的、每个篮子中只含有两种类型水果的最长连续段的长度。

时间复杂度与空间复杂度

  • 时间复杂度:O(n),其中 n 是数组 fruits 的长度。每个元素最多被遍历一次。
  • 空间复杂度:O(1),虽然使用了哈希表,但由于最多只存储两种类型的水果,所以哈希表的大小是常数级别,整体空间复杂度为 O(1)。

代码

public int totalFruit(int[] fruits) {
        Map<Integer,Integer> map=new HashMap<>();
        int ret=0;
        for(int left=0,right=0;right<fruits.length;right++){
            map.put(fruits[right],map.getOrDefault(fruits[right],0)+1);
            while(map.size()>2){
                map.put(fruits[left],map.get(fruits[left])-1);
                if (map.get(fruits[left])==0){
                    map.remove(fruits[left]);
                }
                left++;
            }
            ret=Math.max(ret,right-left+1);
        }
        return ret;
    }

 举例

测试用例 [1,2,3,2,2]

初始状态

  • 初始化 left = 0right = 0ret = 0,以及一个空的哈希表 map 用于记录每种水果的数量。

执行过程

  1. 右指针移动与计数

    • 当 right = 0fruits[right]=1,哈希表变为 {1:1}
    • 当 right = 1fruits[right]=2,哈希表变为 {1:1, 2:1}
    • 当 right = 2fruits[right]=3,哈希表变为 {1:1, 2:1, 3:1}。此时哈希表大小超过2,触发左指针移动。
  2. 左指针移动与计数调整

    • 移动 left,从 left = 0 开始,哈希表变为 {1:0, 2:1, 3:1},然后移除键值对 {1:0},哈希表变为 {2:1, 3:1},此时 left = 1
  3. 继续右指针移动

    • 当 right = 3fruits[right]=2,哈希表变为 {2:2, 3:1}
    • 当 right = 4fruits[right]=2,哈希表变为 {2:3, 3:1}。此时窗口内还是两种水果,不触发左指针移动。

结果计算

  • 在整个过程中,不断更新 ret 以记录最长子数组长度。当右指针遍历结束时,ret 记录的是满足条件的最长子数组长度。
  • 对于测试用例 [1,2,3,2,2],最长的连续子数组内包含两种水果的最大长度为从索引1到4,即 [2,3,2,2],长度为4。

返回结果

  • 因此,这段代码在处理完测试用例后返回的结果是 4

🦣二.找到字符串中的所有字母异位词

题目链接:438.找到字符串中所有字母异位词

算法原理

  1. 初始化:首先将输入的字符串 sspp 分别转换成字符数组 sp,便于后续操作。然后,创建两个大小为 26 的整型数组 hash1hash2 作为哈希表,用于记录字符计数。hash2 用于预存储字符串 pp 中各字符出现的频次。

  2. 预处理:遍历字符串 pp,在 hash2 中记录每个字符出现的次数。比如,如果 pp 是 "abc",那么 hash2['a' - 'a'](即 hash2[0])将增加 1,以此类推。

  3. 滑动窗口遍历

    • 使用两个指针 left 和 right 初始化为 0,定义一个 count 记录当前窗口内满足条件的字符数量。
    • 当 right 指针遍历 s 时,更新 hash1 中对应字符的计数,并检查如果该字符的计数不大于 hash2 中的计数,则增加 count
    • 当窗口大小(right - left + 1)超过 pp 的长度时,说明需要收缩窗口左侧。此时,检查 left 指针所指字符的计数,如果它之前满足条件(即计数不大于 hash2 中的计数),则减少 count,并减少 hash1 中该字符的计数,然后 left 指针右移。
    • 当窗口内满足条件的字符数 count 等于 pp 的长度时,说明找到了一个异位词,将 left 索引添加到结果列表 ret 中。
  4. 返回结果:遍历结束后,返回存储了所有异位词起始索引的列表 ret

时间复杂度与空间复杂度

  • 时间复杂度:O(N),其中 N 为字符串 ss 的长度。每个字符最多被遍历一次。
  • 空间复杂度:O(1),虽然使用了两个大小为 26 的数组作为哈希表,但它们的空间需求是固定的,不依赖于输入字符串的大小。

代码

   public List<Integer> findAnagrams(String ss, String pp) {
        List<Integer> ret= new ArrayList<>();
        char[] s=ss.toCharArray();
        char[] p=pp.toCharArray();
        int[] hash1=new int[26];
        int[] hash2=new int[26];
        //将pp对照的范本储存在hash2
        for (char a:p){
            hash2[a-'a']++;
        }
        int len=p.length;
        for(int left=0,right=0,count=0;right<s.length;right++){
            hash1[s[right]-'a']++;
            if (hash1[s[right]-'a']<=hash2[s[right]-'a']){
                count++;
            }
            if (right-left+1>len){
                if (hash1[s[left]-'a']<=hash2[s[left]-'a']){
                    count--;
                }
                hash1[s[left++]-'a']--;
            }
            if (count==len){
                ret.add(left);
            }
        }
        return ret;
    }

举例

测试用例 s = "cbaebabacd" ; p ="abc"
  1. 初始化: 定义两个长度为 26 的列表 hash1hash2 用于存储字符计数。hash2 用于存储模式字符串 p 中每个字符出现的次数。

  2. 构建模式哈希: 遍历模式字符串 p,并使用 ASCII 值减去 'a' 的 ASCII 值作为索引来记录每个字符的出现次数到 hash2 中。

  3. 双指针遍历:

    • 初始化两个指针 left 和 right 以及一个计数器 count 来追踪当前窗口是否为有效异位词。
    • 使用 right 指针向右移动,增加右侧字符在 hash1 中的计数,如果该字符的计数小于等于 hash2 中的计数,则增加 count
    • 当窗口大小(即 right - left + 1)超过模式字符串长度时,说明需要收缩窗口:
      • 减少左侧字符在 hash1 中的计数,如果之前该字符的计数也小于等于 hash2 中的计数,则减少 count
      • 左指针 left 向右移动一位,缩小窗口。
    • 检查 count 是否与模式字符串长度相等,如果相等则说明当前窗口是一个有效异位词,记录下其起始索引 left
  4. 收集结果: 所有满足条件的起始索引被收集到列表 ret 中,并最终返回。

现在,针对给定的测试用例:

  • 字符串 s = "cbaebabacd"

  • 模式 p = "abc"

  • 遍历开始,首先构建 hash2hash2 对于 "abc" 会是 [1, 1, 1](因为每个字符出现一次)。

  • 双指针开始滑动:

    • 当 right = 0 时,"c" 计数增加,但不满足异位词条件(因为没有比较字符)。
    • 移动到 "b" (right = 1),此时窗口 "cb",仍然不构成异位词。
    • 添加 "a" (right = 2),窗口变为 "cba",这时 hash1 与 hash2 匹配(各字符计数都是 1),因此 count = 3(与模式长度相等),记录索引 0
    • 继续滑动,当 "e" 进入窗口时(right = 3),由于它不在模式中,之前 "c" 的计数减一并不影响 count(仍为 3),但窗口大小已超过模式长度,所以收缩左侧。
    • 收缩窗口时,移除 "c" (left = 1),检查并可能更新 count,但在这个例子中,直到 "b" (left = 2) 被移除时 count 才会减少,因为它与模式中的字符匹配。
    • 当窗口滑动到最后,到达 "bac"(在原字符串中的位置为索引 6 开始),再次满足异位词条件,记录索引 6

最终,函数正确返回了异位词子串的起始索引 [0, 6],这与我们的手动分析相符。

🐮三.串联所有单词的子串

题目链接:30.串联所有单词的子串

算法原理

  1. 初始化:

    • 创建一个结果列表 ret 用来存放符合条件的子串起始索引。
    • 创建一个哈希表 hash1 来记录单词数组 words 中每个单词出现的次数。
    • 计算单个单词的长度 len 和单词数组的大小 size
  2. 构建单词频率哈希表:

    • 遍历 words 数组,使用哈希表 hash1 记录每个单词及其出现次数。
  3. 滑动窗口遍历字符串:

    • 以单词长度 len 为步长,从字符串 s 的每个可能的起始位置开始滑动窗口。
      • 对于每个起始位置 i,初始化一个新的哈希表 hash2 来记录窗口内各个单词的出现次数。
      • 使用两个指针 left 和 right 表示窗口的左右边界,初始时 left = right = i
      • 移动 right 指针,每次向右移动 len 个单位,将新进入窗口的单词 in 计数加入 hash2
        • 如果 hash2 中 in 的计数不大于 hash1 中的计数,说明当前单词匹配成功,计数器 count 加一。
      • 当窗口大小(即 right - left + 1)超过所有单词组合的总长度(即 len * size)时:
        • 移出窗口最左边的单词 out,更新 hash2 并相应减少 count(如果必要)。
        • 同时,左边界 left 向右移动 len 个单位,缩小窗口。
      • 如果 count 等于 size,说明窗口内的单词完全匹配了 words 中的所有单词(考虑顺序和数量),将 left 索引添加到结果列表 ret 中。
  4. 返回结果: 遍历完成后,返回包含所有符合条件子串起始索引的列表 ret

时间复杂度分析

  • 外层循环遍历字符串 s,时间复杂度为 O(n),其中 n 是字符串 s 的长度。
  • 内层循环虽然也是遍历字符串,但由于每次滑动窗口实际上是在“跳跃”(每次跳过一个单词长度),其复杂度受到单词长度和单词数组大小的影响,大致可视为 O(m*k),其中 m 是单词数组的大小,k 是单个单词的平均长度。
  • 因此,总体时间复杂度大约为 O(nmk)。

空间复杂度

  • 主要由哈希表占用,空间复杂度为 O(m+k),其中 m 是单词数组的大小,k 是不同单词的数量(在极端情况下,所有单词都不重复)。

代码

public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ret = new ArrayList<>();
        Map<String,Integer> hash1=new HashMap<>();
        int len=words[0].length();
        int size=words.length;
        for(String str:words){
            hash1.put(str,hash1.getOrDefault(str,0)+1);
        }
        for(int i=0;i<len;i++){
            Map<String,Integer> hash2=new HashMap<>();
            for(int left=i,right=i,count=0;right+len <=s.length();right+=len){
                String in=s.substring(right,right+len);
                hash2.put(in,hash2.getOrDefault(in,0)+1);
                if(hash2.get(in)<=hash1.getOrDefault(in,0)){
                    count++;
                }
                if(right-left+1>len*size){
                    String out=s.substring(left,left+len);
                    if(hash2.get(out)<=hash1.getOrDefault(out,0)){
                    count--;
                    }
                    hash2.put(out,hash2.get(out)-1);
                    left+=len;
                }
                if(count==size){
                    ret.add(left);
                }
            }
        }
            return ret;
    }

举例

测试用例 s ="barfoofoobarthefoobarman";words =["bar","foo","the"]
  1. 初始化:

    • 初始化结果列表 ret
    • 创建哈希表 hash1 存储单词及其计数,初始化为空。
    • 单词长度 len = 3(因为 words[0].length() 是 "bar" 的长度)。
    • 单词数组大小 size = 3(因为有三个单词)。
    • 遍历 words,填充 hash1,使其内容为 {bar=1, foo=1, the=1}
  2. 滑动窗口遍历:

    • 关键在于理解滑动窗口如何准确地识别出所有匹配的子串起始位置。

    • 第一个匹配:

      • 窗口从索引 0 开始,但首个匹配实际上从索引 6 开始,因为 "foobarthefoobarman" 中从索引 6("f") 开始的子串包含了 "foo""bar""the"(按照 "foobarthe" 的顺序)。此时,count 达到 size(3),因此 6 被添加到结果列表中。
    • 第二个匹配:

      • 窗口继续右移,当右边界达到索引 9 时,子串变为 "oobarthefoo",其中包含了另一个 "foo""bar""the" 序列(注意,尽管 "foo" 重叠,但因为我们是寻找无序的组合,所以依然有效)。此时,也会触发 count == size,因此索引 9 被记录。
    • 第三个匹配:

      • 最后一个匹配发生在索引 12,从这里开始的子串为 "obarfoobarman",同样包含了所需的三个单词(忽略顺序)。因此,索引 12 也被加入结果列表。
  3. 结果收集:

    • 正确的输出是 [6, 9, 12],这三个位置开始的子串分别包含了给定单词数组 ["bar", "foo", "the"] 中所有单词的一个排列。

 

🐏 四.最小覆盖串

题目链接:76.最小覆盖子串

算法原理

  1. 初始化:

    • 将输入字符串 ss 和 tt 转换为字符数组 s 和 t 以便于操作。
    • 初始化两个哈希表(在这里使用整型数组 hash1 和 hash2)来记录字符出现的次数。数组的索引对应字符的 ASCII 码值,值表示字符出现的次数。
    • 初始化变量 kind 记录需要匹配的字符种类数(即 tt 中不同字符的数量),初始化最小长度 minlen 为最大整数值,记录子串起始索引 begin 为 -1
    • 遍历字符串 tt,统计每个字符的出现次数到 hash1,同时增加 kind 的值,表示需要匹配的字符种类。
  2. 滑动窗口遍历:

    • 使用两个指针 left 和 right 构成一个滑动窗口,初始时 left = right = 0
    • 当 right 指针遍历 s 时,对进入窗口的字符 in 增加 hash2 的计数,若该字符在 hash2 中的计数等于其在 hash1 中的计数,则说明这个字符已经匹配完成,增加计数器 count
    • 当 count 等于 kind 时,说明窗口内的字符已经包含了 tt 中所有字符至少各一次,此时尝试缩小窗口以寻找最小覆盖子串:
      • 检查当前窗口长度是否小于已记录的最小长度 minlen,若是,则更新 minlen 和子串起始索引 begin
      • 然后,从窗口左侧移除字符 out(即 s[left]),并更新 hash2 和 count,随后移动 left 指针向右,继续寻找可能更小的覆盖子串。
  3. 结果处理:

    • 循环结束后,检查是否有找到符合条件的子串,如果没有(即 begin == -1),返回空字符串;否则,返回 ss 中从 begin 开始长度为 minlen 的子串。

算法特性

  • 时间复杂度: O(N),其中 N 是字符串 ss 的长度。每个字符最多被访问两次:一次作为窗口的右边界,一次作为窗口的左边界。
  • 空间复杂度: O(1),尽管使用了额外的哈希表,但因为字符集固定(ASCII码范围),所以空间复杂度是常数级别的。

代码

 public String minWindow(String ss, String tt) {
        char[] s=ss.toCharArray();
        char[] t=tt. toCharArray();
        int[] hash1=new int[128];
        int[] hash2=new int[128];
        int kind=0;
        int minlen=Integer.MAX_VALUE;
        int begin=-1;
        for(char ch:t){
           if(hash1[ch]++==0)kind++; 
        }
        for(int left=0,right=0,count=0;right<s.length;right++){
            char in=s[right];
            if(++hash2[in]==hash1[in])count++;
            while(kind==count){
                if(right-left+1<minlen){
                    begin=left;
                    minlen=right-left+1;
                }
                char out=s[left];
                if(hash2[out]--==hash1[out])count--;
                left++;
            }
        }
        if(begin==-1){
            return new String(); 
        }
        return ss.substring(begin,begin+minlen);
    }

举例

测试用例 s ="ADOBECODEBANC" ; t ="ABC"
  1. 初始化:

    • 将字符串 s 和 t 转换为字符数组。
    • 初始化两个长度为 128 的数组 hash1 和 hash2 作为哈希表,记录字符出现次数。
    • 初始化变量 kind 为 0,用于记录目标字符串 t 中不同字符的种类数;minlen 设置为 Integer.MAX_VALUE,用于记录最小覆盖子串长度;begin 设置为 -1,用于记录最小覆盖子串的起始位置。
    • 遍历目标字符串 t,对 hash1 进行填充并计算种类数 kind。在此例中,hash1['A'] = 1hash1['B'] = 1hash1['C'] = 1,因此 kind = 3
  2. 滑动窗口遍历:

    • 使用双指针 left 和 right 构建滑动窗口,初始时 left = 0right = 0count = 0
    • 遍历字符串 s
      • 当右指针 right 移动时,遇到的字符 in = s[right],在 hash2 中计数递增,如果这个递增使得 hash2[in] 等于 hash1[in],说明字符 in 已经匹配完成,于是 count++
      • 当 count 等于 kind 时,说明窗口内的字符已经包含了 t 中所有字符至少各一次。此时开始收缩窗口:
        • 检查当前窗口长度是否小于 minlen,若是,则更新 minlen 和 begin
        • 移除窗口左侧的字符 out = s[left],在 hash2 中对应的计数递减,如果递减后 hash2[out] 等于 hash1[out],说明移除的字符不再满足匹配条件,于是 count--
        • 然后 left++,继续检查下一个可能的窗口。
    • 在这个过程中,对于测试用例,窗口会遍历并调整位置,直到找到包含 "ABC" 所有字符的最小子串。具体来说,当窗口覆盖 "BANC" 时,满足条件,此时 count = 3,并且窗口长度是最小的。
  3. 结果处理:

    • 最终,当遍历结束,如果找到了有效的子串(即 begin != -1),根据记录的 begin 和 minlen 截取并返回最小覆盖子串。对于给定测试用例,返回结果是 "BANC"

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值