【java算法专场】滑动窗口(下)

目录

水果成篮

算法分析

算法步骤

示例

算法代码

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

算法分析

算法步骤

示例

算法代码

优化

算法代码

串联所有单词的子串

算法分析

算法步骤

示例

算法代码

 最小覆盖子串

算法分析

算法步骤

示例

算法代码


水果成篮

算法分析

这道题其实就是在数组中找连续的的最长子数组,且子数组里的不同数字不能超过两个。 如果采用暴力枚举数组中的每一个字串,并借助哈希表来存储子数组中有多少种水果,并且每种水果的数量有多少。时间复杂度会达到O(n),因此,可以在暴力枚举的举出上进行优化,使用滑动窗口来解决。

算法步骤

  1. 初始化:设置双指针leftright并初始化为0,表示滑动窗口的边界。设置一个空的HashMap来存储水果种类及数量。定义ans并初始化为0,用于记录能够采摘的数目。
  2. 右边界(right)移动:让right往右移动,并将nums[right]在HashMap中的数量+1
  3. 判断水果种类:当HashMap.size()>2时,说明此时采摘的水果中有3种,需要去除一种。此时就需要让left往右移动,并将nums[left]在HashMap中的数量-1,当nums[left]对应的数量为0时,说明此时已经将一种水果去除,调用 map.remove(fruits[left]),将水果移除。
  4. 更新结果:在右指针right遍历数组的时,每次都需要更新一下ans的值,判断ans此时的值是否比(right-left+1)大,若大于则进行更新。
  5. 遍历结束:当right遍历完数组,此时返回ans的值即可。

时间复杂度:O(n),n是数组长度,每个元素最多只会被遍历两次。在扩大窗口的时候,right会遍历一遍数组。在最坏情况下,left也只会遍历一遍数组。

空间复杂度:O(1),这里只用到了几个固定的变量,虽然使用了HashMap,但最多也只存储了三种。

示例

以[1,2,3,2,2]为例

第一步:初始化

让left=right=0,定义一个map(哈希表),初始化ans=0;

第二步:扩大窗口,判断种类

让right往右移,在map中不超过2种时,同时更新ans值。

 第三步:左指针移动

此时map中种类超过2种,需要移动左指针,直到map中某个水果的数量为0。将数量为0的水果从map中移除。

 第四步:右指针继续移动,重复二三步,直到右指针right遍历完数组。当遍历完数组,此时ans=4.即在数组中[2,3,2,2]。

第五步:返回结果,将ans=4返回。

算法代码

/**
 * 计算最长的水果数组段,其中只包含两种不同的水果。
 * 
 * @param fruits 水果数组,数组中的每个元素代表一种水果。
 * @return 返回最长的水果数组段的长度。
 */
public int totalFruit(int[] fruits) {
    /* 初始化结果变量为0,用于记录最长的水果数组段的长度 */
    int ans = 0;
    /* 初始化左指针为0,用于标记当前水果数组段的起始位置 */
    int left = 0;
    /* 初始化右指针为0,用于标记当前水果数组段的结束位置 */
    int right = 0;
    /* 使用HashMap来记录每种水果的数量,键为水果的类型,值为该类型水果的数量 */
    HashMap<Integer, Integer> map = new HashMap<>();
    /* 遍历水果数组,更新HashMap和计算最长数组段的长度 */
    for (; right < fruits.length; right++) {
        /* 将右指针指向的水果添加到HashMap中,如果该水果已存在,则数量加1 */
        map.put(fruits[right], map.getOrDefault(fruits[right], 0) + 1);
        /* 当HashMap中的水果种类超过2种时,需要缩小数组段直到满足条件 */
        while (map.size() > 2) {
            /* 从HashMap中减去左指针指向的水果的数量,如果数量减到0,则从HashMap中移除该水果 */
            map.put(fruits[left], map.getOrDefault(fruits[left], 0) - 1);
            if (map.get(fruits[left]) == 0) {
                map.remove(fruits[left]);
            }
            /* 左指针向右移动,缩小数组段 */
            left++;
        }
        /* 更新最长数组段的长度 */
        ans = Math.max(ans, right - left + 1);
    }
    /* 返回最长数组段的长度 */
    return ans;
}

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

算法分析

这道题题意说明了要在字符串s中找到所有p的异位词。所以这道题可以使用暴力枚举+哈希表,但是时间复杂度达到O(n^2),我们可以使用滑动窗口+哈希表来使用。

算法步骤

  1. 初始化:定义双指针leftright并初始化为0,将两个字符串s和p转化为字符数组ss和pp,且这里需要用到两个hash表,这里设置为hash1和hash2,hash1用来存放字符串p的字符个数,hash2用来后续的遍历操作。【这里由于限制了是小写字母,所以hash数组的大小可以设置为26】定义变量m记录p的长度。定义ans顺序表,用来存放在符合条件的子串的起始位置。
  2. 扩展右边界:右指针right往右移动,用变量in记录字符nums[right],将in字符存进hash2中,但需要注意在存放时需要让in-‘a’,即hash2[in-'a']。
  3. 处理窗口:由于这里是限定了窗口的大小,即字符串p的长度。所以当right-left+1大于m时,说明窗口过大,需要进行缩小窗口,让left往右移动一位,同时需要将out【这里定义变量out字符为hash2[left]】在hash2中➖1。当right-left+1==m,说明此时窗口大小刚好等于p的长度,调用equal方法来判断hash1和hash2是否相等,若相等,则将left添加到ans中

时间复杂度:O(n),n为s的长度,每个字符最多被访问2次。

空间复杂度:O(1),虽然使用hash1和hash2数组,但都是固定大小的,且使用的变量固定。

示例

算法代码

 /**
      * 查找字符串s中所有p的字母异位词的起始索引。
      *
      * @param s 输入的字符串
      * @param p 指定的模式字符串
      * @return 返回一个包含所有字母异位词起始索引的列表
      */
     public List<Integer> findAnagrams1(String s, String p) {
         List<Integer> ans = new ArrayList<>(); // 用于存储结果的列表
         int left = 0; // 窗口的左边界
         int right = 0; // 窗口的右边界
         char[] ss = s.toCharArray(); // 将字符串s转换为字符数组
         char[] pp = p.toCharArray(); // 将字符串p转换为字符数组
         int[] hash1 = new int[26]; // 用于存储模式字符串p的字符计数
         for (char ch : pp) hash1[ch - 'a']++; // 统计模式字符串p中每个字符的数量

         int[] hash2 = new int[26]; // 用于存储当前窗口中字符的计数
         int m = pp.length; // 模式字符串p的长度
         while (right < s.length()) { // 窗口右边界不超过字符串s的长度
             char in = ss[right]; // 当前纳入窗口的字符
             hash2[in - 'a']++; // 窗口内字符计数加一
             if (right - left + 1 > m) { // 当窗口大小超过模式字符串p的长度时
                 char out = ss[left++]; // 移除窗口最左边的字符
                 hash2[out - 'a']--; // 窗口内字符计数减一
             }
             if (right - left + 1 == m) { // 当窗口大小等于模式字符串p的长度时
                 if (Arrays.equals(hash1, hash2)) { // 比较窗口内字符计数和模式字符串的字符计数
                     ans.add(left); // 如果相同,则将窗口左边界添加到结果列表中
                 }
             }
             right++; // 窗口右边界向右移动
         }
         return ans; // 返回结果列表
     }

优化

当然,在上述中,若面对比较复杂的问题,不是最优解法,可以对其进行优化。

主要是第二、三步进行处理

在上述算法中,我们是将ss[right]的值直接放在hash2中,但我们可以进行优化:

  • 定义一个count,用来判断判断当前窗口是否符合条件,当count=m时,说明找到了符合条件的子串,将left添加到ans中即可;count++的前提条件是hash2中的计数小于等于hash1中的计数,才能让count++。
  • 当right-left+1>m,即窗口过大时,要缩小窗口,但同时需要判断此时以ss[left]为下标的hash2中的计数是否小于等于hash1中对应位置的计数,若小于,则需要让count--。

算法代码


     /**
      * 查找字符串s中所有p的字母异位词的起始索引。
      *
      * @param s 输入的字符串
      * @param p 指定的字母异位词模式
      * @return 返回一个包含所有字母异位词起始索引的列表
      */
     public List<Integer> findAnagrams(String s, String p) {
         List<Integer> ans = new ArrayList<>();
         char[] ss = s.toCharArray();
         char[] pp = p.toCharArray();

         // 初始化hash1数组,用于存储模式p中每个字符出现的次数
         int[] hash1 = new int[26];
         for (char ch : pp) {
             hash1[ch - 'a']++;
         }

         // count用于记录当前窗口中满足p中字符比例的子串数量
         int count = 0;
         int m = pp.length;
         // hash2数组用于存储当前窗口中每个字符出现的次数
         int[] hash2 = new int[26];
         int left = 0;
         int right = 0;

         // 滑动窗口遍历字符串s
         for (; right < s.length(); right++) {
             char in = ss[right];
             // 当前字符加入窗口,如果其出现次数不超过p中对应字符的次数,则count增加
             // 判断是否满足条件,进窗口
             if (++hash2[in - 'a'] <= hash1[in - 'a']) {
                 count++;
             }

             // 如果窗口大小超过了p的长度,则需要移除最左边的字符
             // 判断长度,
             if (right - left + 1 > m) {
                 char out = ss[left++];
                 // 移除字符,如果移除后窗口中该字符出现次数不超过p中对应字符的次数,则count减少
                 if (hash2[out - 'a']-- <= hash1[out - 'a']) {
                     count--;
                 }
             }

             // 如果当前窗口中满足p中字符比例的子串数量等于p的长度,则当前窗口起始索引加入结果列表
             if (count == m) {
                 ans.add(left);
             }
         }

         return ans;
     }

串联所有单词的子串

算法分析

这道题与前一道题优化算法的思路基本一样,要求在字符串s中找到words字符串数组中所有字符串任意顺序串联起来的子串。

算法步骤

  1. 初始化:设置双指针leftright并初始化为0;设置两个HahsMap<String,Integer>,map1用来存放words字符串数组中的字符串及其个数,map2用来存放s字符串中符合条件的字符串;设置ans顺序表,用来存放符合条件的子串起始位置。设置count并初始化为0,用来记录符合条件的子串个数;定义len并初始化为words[0].length()[即字符串数组中字符串的长度],定义m并初始化为words数组的长度
  2. 预处理:将words字符串数组中的字符串存放到map1中。
  3. 滑动窗口遍历字符串
  • 由于在s字符串中,我们不知道words数组中字符串在s中是在起始位置上开始的,所以根据len,可以有len种起始位置。示例:

  • 每个单词的长度为len,所以设置双指针在移动时,步长为len。
  • 遍历每一个有可能的起始位置i,并每次定义map2用来存放不同起始位置下的单词。 
  • 让left=right=i,count=0,移动窗口,并将分割出来的单词in放进map2中,判断in在map2中的数量是否小于等于map1的数量,若小于,则让count++;
  • 判断当前窗口的长度(right-left+1)是否大于字符串数组中所有字符串的总长度(len*m),若大于,说明窗口过大,需要将【left,left+len】位置的字符出窗口。需要出窗口的字符串out,需要判断是否在map2中的数量小于等于map1的数量,若小于,则让count--;当出完窗口后,需要让out在map2中对应的数量-1,再让left+=len;
  • 当count等于m【words数组的长度】,说明此时已经找到了符合条件的串联子串,将left添加到ans中。

4.返回结果:当遍历完所有可能的情况,此时返回结果ans即可。

示例

以s = "barfoothefoobarman", words = ["foo","bar"]为例

第一步:初始化,并预处理

在初始化完所需的变量之后,将words数组中的字符串存放到map1中,得到

map1={["foo",1],["bar",1]}

第二步:滑动窗口遍历s(此处只列举第一种起始位置情况)

  1. 此时left=right=i=0,count=0(此处len为3)
  2. 截断s中[right,right+len)位置的字符串作为in,in=“bar”,将in存放到map2中,map2={["bar",1]},并且判断一下in在map2中的数量是否小于等于在map1中的数量,若小于等于,则让count++;
  3. 判断窗口的大小(right-left+1=3-0+1=4<len*m=4*2=8,不需要进行出窗口操作。
  4. 由于count此时依旧为0,小于m=2,不满足条件,left不用添加到ans中。
  5. 重复上述操作。
  6. 第二次:map2={["bar",1],["foo",1]} count=2  添加left到ans中 ans=[0]
  7. 第三次:map2={["bar",1],["foo",1],["the",1]}  count=2 
  8. 出窗口:map2={["foo",1],["the",1]}  count=1 
  9. 第四次: map2={["foo",2],["the",1]}  count=1
  10. 出窗口: map2={["foo",1],["the",1]}  count=1
  11. 第五次: map2={["foo",1],["the",1],["bar",1]}  count=2  
  12. 出窗口:map2={["foo",1],["bar",1]}  count=2 添加left到ans中,此时left=9 ans=[0,9]
  13. 第六次:  map2={["foo",1],["bar",1],["man",1]}  count=2
  14. 出窗口:map1={["bar",1],["man",1]}  count=1
  15. 此时right已经走到s的末尾,遍历结束,返回结果ans=[0,9]。

算法代码

 /**
      * 查找所有包含给定单词数组中所有单词的子字符串的起始索引。
      *
      * @param s 输入的字符串。
      * @param words 给定的单词数组。
      * @return 包含所有单词的子字符串的起始索引列表。
      */
     public List<Integer> findSubstring(String s, String[] words) {
         // 用于存储结果的列表
         List<Integer> ans = new ArrayList<>();
         // 统计每个单词出现的次数
         // 借助hash表
         HashMap<String,Integer> map1=new HashMap<>();
         //统计words中有多少单词
         for(String word:words) map1.put(word,map1.getOrDefault(word,0)+1);
         //用于后序操作

         // 单词的长度
         int len=words[0].length();// 单词长度
         // 单词的数量
         int m=words.length;
         // 当前窗口中包含的所有单词的数量
         int count=0;
         // 窗口的左右边界
         int left=0;
         int right=0;
         // 遍历字符串s,寻找符合条件的子字符串
         for(int i=0;i<len;i++) {
             // 当前窗口中单词的统计信息
             HashMap<String, Integer> map2 = new HashMap<>();
             // 移动窗口,查找符合条件的子字符串
             for (left = i, right = i,count=0; right+ len <= s.length() ; right += len) {
                 // 窗口中的当前单词
                 //进窗口维护
                 String in = s.substring(right, right + len);
                 // 更新当前单词在窗口中的统计信息
                 map2.put(in, map2.getOrDefault(in, 0) + 1);
                 // 如果当前单词在窗口中的数量不超过其应有数量,则增加count
                 if (map2.get(in) <= map1.getOrDefault(in, 0)) count++;
                 // 如果窗口大小超过了最大允许值,则需要移除左边界上的单词
                 //出窗口
                 if (right - left + 1 > len*m) {
                     // 移除的单词
                     String out = s.substring(left, left + len);
                     // 如果移除的单词在窗口中的数量不超过其应有数量,则减少count
                     if (map2.get(out) <= map1.getOrDefault(out, 0)) count--;
                     // 更新窗口中移除单词的统计信息
                     map2.put(out, map2.get(out) - 1);
                     // 移动左边界
                     left += len;
                 }
                 // 如果当前窗口中包含了所有单词,则将左边界索引添加到结果列表中
                 if (count == words.length) ans.add(left);
             }
         }
         // 返回结果列表
         return ans;
     }

 最小覆盖子串

算法分析

本题要求在s中找到t的最小覆盖子串,我们可以使用暴力枚举+hash表来实现,但时间复杂度会达到O(n^2),我们可以在此进行优化,使用滑动窗口。

算法步骤

  1. 初始化:设置双指针leftright,并初始化为0作为滑动窗口的边界;将s和t字符串转化为字符数组ss和tt方便操作;设置两个hash表(hash1和hash2),长度为128,hash1用来统计t中的字符;设置begin并初始化为-1,mixLen初始化为Integer.MAX_VALUE,作为最小覆盖子串的起始位置以及末尾位置;定义count(计数器)并初始化为0,用来匹配符合条件的字符。

  2. 预处理:设置kind并初始化为0,用来计算t中的字符种类有多少种。将tt字符数组中的字符通过hash1进行计数,当hash[ch]为0时,kind++;

  3. 处理窗口:右指针移动,并将in【in=ss[right]】添加到hash2中,同时判断in在hash2中的数量是否与hash1的相同,若相同,则让count++。当计数器count等于kind时,说明已经找到了符合条件的子串,此时,若minLen==-1或者right-left+1<minLen时,更新begin=left和minLen=right-left+1;当添加完后,缩小窗口,让left往右移,若hash2[left]==hash1[left],需要让count--,同时让left++;寻找更小的覆盖子串。

  4. 重复上述操作,直到遍历完s。

  5. 返回结果:若minLen==-1,说明没有找到符合条件的子串,如不为0,则返回[begin,begin+minLen)的截断字符串。

时间复杂度:O(n),n为ss的长度,每个字符最多被遍历两次。

空间复杂度:O(1),虽然使用了hash,但长度是固定的。

示例

以s = "ADOBECODEBANC", t = "ABC"为例

第一步:初始化

  • left=right=0
  • 初始化hash1和hash2并设置长度为128.
  • 将s和t转化为字符数组ss和tt
  • kind=0,用于记录不同字符的种类,遍历数组tt并计数到hash1中,此处kind=3,hash['A']=1,hash['B']=1,hash['C']=1.

第二步:处理窗口

  1. 使用双指针left和right,扩大窗口,并将字符in计数到hash2中,若in在hash2的数量等于hash1的数量,则让count++;
  2. 当count=kind时,说明此事已经找到了匹配的覆盖子串,判断right-left+1<minLen或者minLen=-1时,则进行替换,让begin=left,minLen=right-left+1;当判断完之后,移除左窗口out,同时判断out在hash2和hash1的计数是否相等,若相等,则让count--。
  3. 重复上述操作,当right遍历完s字符串时,结束循环。
  4. 返回结果,此时,由于begin不为-1,根据begin和minLen,截取s中此位置的字符串,为“BANC”。

算法代码

  /**
      * 寻找字符串s中包含字符串t所有字符的最短子串。
      * @param s 原始字符串
      * @param t 目标字符串,需要在原始字符串中找到包含所有这些字符的最短子串
      * @return 返回最短子串的字符串,如果不存在则返回空字符串
      */
     public String minWindow(String s, String t) {
         // 初始化两个字符数组,用于统计字符串t和字符串s中字符出现的次数
         int[] hash1=new int[128];
         int[] hash2=new int[128];
         
         // 将字符串t转换为字符数组,方便后续处理
         char[] tt=t.toCharArray();
         
         // 计算字符串t中不同字符的数量
         int kind=0;
         for(char ch:tt){
             if(hash1[ch]++==0) kind++;
         }
         
         // 将字符串s转换为字符数组,方便后续处理
         char[] ss=s.toCharArray();
         
         // 初始化指针begin和minlen,begin用于标记最短子串的起始位置,minlen用于记录最短子串的长度
         int begin=-1;
         int minlen=Integer.MAX_VALUE;
         
         // 初始化滑动窗口的左指针left、右指针right和当前窗口中包含t中字符的数量count
         int left,right,count;
         for(left=0,right=0,count=0;right<ss.length;right++){
             char in=ss[right];
             // 当前字符在窗口中出现的次数等于在t中出现的次数时,count加1
             if(++hash2[in]==hash1[in]) count++;
             
             // 当窗口中包含了t中所有字符时
             while(count==kind){
                 // 更新最短子串的起始位置和长度
                 if(right-left+1<minlen||minlen==-1){
                     begin=left;
                     minlen=right-left+1;
                 }
                 // 移动窗口的左指针,并更新count和hash2数组
                 char out=ss[left++];
                 if(hash2[out]--==hash1[out]) count--;
             }
         }
         
         // 根据begin和minlen的值,返回最短子串或空字符串
         if(begin==-1){
             return  new String();
         }else{
             return s.substring(begin,begin+minlen);
         }
     }

以上就是本篇所有内容,滑动窗口的题目就先到这了,后序若有相关题目,将会更新!

若有不足,欢迎指正~ 

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小猪同学hy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值