数组-滑动窗口(直接套模板完事儿)

前言

兄弟们,互联网寒冬期,算法刷着走。上篇文章讲了双指针的左右指针,双指针是数组类算法题中最重要的一个分支之一。这篇文章讲双指针技巧的滑动窗口。遇到双指针的题目,直接套用模板就完事儿。另外,数组有下图这些知识点与技巧。

思路

滑动窗口一般用于解决主串是否满足子串的某些需求问题,比如,是否包含某个字串,是否含有字串的所有字符等。
滑动窗口有固定的解题模板。但是细节众多,需要反复练习。

//s为原字符串,t为子字符串
public void slidingWindow(String s, String t) {
    //初始化窗口应该满足的条件needMap,key=子串t中的字符,value=字符在t字符串中出现的次数
    Map<Character, Integer> needMap = new HashMap<>();
    for (int i = 0; i < t.length(); i++) {
        Character k = t.charAt(i);
        needMap.put(k, needMap.getOrDefault(k, 0) + 1);
    }
    //滑动窗口中包含的字符,及其数量,key=滑动窗口中的字符,value=字符出现的次数
    Map<Character, Integer> winMap = new HashMap<>();
    //l:窗口左指针,r窗口右指针,validCount:窗口中有多少个字符满足了条件。
    int l = 0, r = 0, validCount = 0;
    while (r < s.length()) {
        //即将加入窗口的字符
        char in = s.charAt(r);
        //窗口右指针右移
        r++;
        //进行窗口内数据的操作
        ...
        while (窗口是否应该收缩) {
            //即将从窗口移除的字符
            char out = s.charAt(l);
            //窗口左指针右移
            l++;
            //进行窗口内数据的操作
            ...
        }
    }
}

最小覆盖子串

leetcode第76题

解题思路

套用滑动窗口模板。窗口左右指针下标从0开始遍历原字符串。

窗口的右指针右移后,判断加入窗口的字符是否是子串的字符,若是则更新winMap。更新后若winMap中该字符的value=needMap中该字符的value,说明滑动窗口中该字符数量与子串该字符的数量相等,则validCount+1。

当validCount=needMap.size()时,说明子串中的字符全部都包含在了滑动窗口中,且每个字符的字符数量也满足。这时就把窗口的左指针右移,直到窗口中的字符串不在符合要求为止。

重复上诉步骤,找出最小的窗口。

复杂度分析

时间复杂度:O(nlogz+mlogz),z是字符集的大小,n是原字符串的长度,m是子串长度。最坏情况下,窗口右指针会扫描一遍原数组,窗口左指针会扫描一边原数组,所以最多扫描2n次,而每扫描一次,就有若干次的map.get与map.put,则复杂度是nlogz。初始化needMap时,需要扫描一遍子串,且也有map.get与map.put。则复杂度为O(nlogz + mlogz)。

空间复杂度:O(z)。因为需要建立两个map分别存滑动窗口与字串中字符的出现次数,且每个map的键值对最多不会存放超过字符集的大小。

代码

class Solution {
    public String minWindow(String s, String t) {
      Map<Character, Integer> needMap = new HashMap<>();
      for (int i = 0; i < t.length(); i++) {
         Character k = t.charAt(i);
         needMap.put(k, needMap.getOrDefault(k, 0) + 1);
      }
      Map<Character, Integer> winMap = new HashMap<>();
      int l = 0, r = 0, validCount = 0, start = 0, minWin = Integer.MAX_VALUE;
      while (r < s.length()) {
         char in = s.charAt(r);
         r++;
         if (needMap.containsKey(in)) {
            winMap.put(in, winMap.getOrDefault(in, 0) + 1);
            //窗口中该字符的数量等于字串中该字符的数量
            if (winMap.get(in).equals(needMap.get(in))) {
               validCount++;
            }
         }
         //窗口中所有字符的数量都大于等于字串中字符的相应数量,说明这是一个满足题意的子串
         while (validCount == needMap.size()) {
             //记录满足条件时的最小窗口
            if (r - l < minWin) {
               start = l;
               minWin = r - l;
            }
            char out = s.charAt(l);
            l++;
            if (needMap.containsKey(out)) {
               winMap.put(out, winMap.getOrDefault(out, 0) - 1);
               //当窗口中该字符的数量小于了字串中的数量
               if (winMap.get(out) < needMap.get(out)) {
                  validCount--;
               }
            }
         }
      }
      return Integer.MAX_VALUE == minWin ? "" : s.substring(start, start + minWin);
   }
}

字符串的排列

leetcode第567题

解题思路

套用滑动窗口模板。窗口左右指针下标从0开始去遍历s2。

窗口的右指针右移后,判断加入窗口的字符是否是s1的字符,若是则更新winMap。更新后若winMap中该字符的value=needMap中该字符的value,说明滑动窗口中该字符数量与s1中的该字符的数量相等,则validCount+1。

当validCount=needMap.size()时,说明s1中的字符全部都包含在了滑动窗口中。此时进行判断,当窗口的长度=s1的长度时,说明是一个合法序列,则返回true。

重复上诉步骤,若遍历结束后都没找到合法序列,则返回false。

复杂度分析

时间复杂度:O(nlogz + mlogz),z是字符集的大小。n 是s1字符串的长度,m是s2字串长度。最坏情况下,窗口右指针会扫描一遍s2,窗口左指针会扫描一遍s2,所以最多扫描2m次,而每扫描一次,就用若干次的map.get与map.put,则复杂度是mlogz。初始化needMap时,需要扫描一遍s1,每次扫描也有map.get与map.put。则复杂度为O(nlogz + mlogz)。

空间复杂度:O(z)。因为需要建立两个map分别存滑动窗口与字串中字符的出现次数,且每个map的键值对最多不会存放超过字符集的大小。

代码

class Solution {
    public boolean checkInclusion(String s1, String s2) {
      Map<Character, Integer> needMap = new HashMap<>();
      for (int i = 0; i < s1.length(); i++) {
         Character k = s1.charAt(i);
         needMap.put(k, needMap.getOrDefault(k, 0) + 1);
      }

      Map<Character, Integer> winMap = new HashMap<>();
      int l = 0, r = 0, validCount = 0;
      while (r < s2.length()) {
         char in = s2.charAt(r);
         r++;
         if (needMap.containsKey(in)) {
            winMap.put(in, winMap.getOrDefault(in, 0) + 1);
            if (winMap.get(in).equals(needMap.get(in))) {
               validCount++;
            }
         }
         while (validCount == needMap.size()) {
            if (r - l == s1.length()) {
               return true;
            }
            char out = s2.charAt(l);
            l++;
            if (needMap.containsKey(out)) {
               winMap.put(out, winMap.getOrDefault(out, 0) - 1);
               if (winMap.get(out) < needMap.get(out)) {
                  validCount--;
               }
            }
         }
      }
      return false;
    }
}

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

leetcode第438题

解题思路

套用滑动窗口模板。窗口左右指针下标从0开始去遍历s。

窗口的右指针右移后,判断加入窗口的字符是否是p的字符,若是则更新winMap。更新后若winMap中该字符的value=needMap中该字符的value,说明滑动窗口中该字符数量与p中的该字符的数量相等,则validCount+1。

当validCount=needMap.size()时,说明p中的字符全部都包含在了滑动窗口中。此时进行判断,当窗口的长度=s的长度时,说明是一个合法序列,则加入列表中。

重复上诉步骤,找出所有的合法序列。

复杂度分析

时间复杂度:O(nlogz + mlogz),n是s字符串的长度,m是p字串长度。最坏情况下,窗口右指针会扫描一遍s,窗口左指针会扫描一遍s,所以最多扫描2n次,而每扫描一次,就用若干次的map.get与map.put,则复杂度是nlogz。初始化needMap时,需要扫描一遍p,每次扫描也有map.get与map.put。则复杂度为O(nlogz + mlogz)。

空间复杂度:O(z)。z是字符集的大小。因为需要建立两个map分别存滑动窗口与字串中字符的出现次数,且每个map的键值对最多不会存放超过字符集大小。

代码

class Solution {
    public List<Integer> findAnagrams(String s, String p) {
      Map<Character, Integer> needMap = new HashMap<>();
      for (int i = 0; i < p.length(); i++) {
         Character k = p.charAt(i);
         needMap.put(k, needMap.getOrDefault(k, 0) + 1);
      }
      Map<Character, Integer> winMap = new HashMap<>();
      List<Integer> list = new ArrayList<>();
      int l = 0, r = 0, validCount = 0;
      while (r < s.length()) {
         char in = s.charAt(r);
         r++;
         if (needMap.containsKey(in)) {
            winMap.put(in, winMap.getOrDefault(in, 0) + 1);
            if (winMap.get(in).equals(needMap.get(in))) {
               validCount++;
            }
         }
         while (validCount == needMap.size()) {
            if (r - l == p.length()) {
               list.add(l);
            }
            char out = s.charAt(l);
            l++;
            if (needMap.containsKey(out)) {
               winMap.put(out, winMap.get(out) - 1);
               if (winMap.get(out) < needMap.get(out)) {
                  validCount--;
               }
            }
         }
      }
      return list;
    }
}

无重复字符的最长子串

leetcode第3题

解题思路

此题不能用常规的滑动窗口模板。由于没有子串,所以不需要needMap去统计子串的字符,也不需要validCount统计窗口内满足子串相关条件的字符数。

窗口左右指针下标从0开始去遍历s。窗口的右指针右移后,将加入窗口的字符更新进winMap。

当winMap中刚加入窗口的字符的value大于1,则说明窗口中有重复的字符了。开始右移左指针,并实时更新winMap,直到winMap中刚加入窗口的字符的value不大于1。此时窗口长度就是一个无重复的子串长度。

重复上诉步骤,找出最大子串长度。

复杂度分析

时间复杂度:O(nlogz),n 是s字符串的长度。最坏情况下,窗口右指针会扫描一遍s,窗口左指针会扫描一遍s,所以最多扫描2n次。而每扫描一次,就用若干次的map.get与map.put,则复杂度为O(nlogz)。

空间复杂度:O(z)。z是字符集的大小。因为需要建立一个map存滑动窗口中字符的出现次数,且map的键值对最多不会存放超过z是字符集的大小。

代码

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> winMap = new HashMap<>();
        int l = 0, r = 0, max = Integer.MIN_VALUE;
        while (r < s.length()) {
            char in = s.charAt(r);
            r++;
            winMap.put(in, winMap.getOrDefault(in, 0) + 1);
            while (winMap.get(in) > 1) {
                char out = s.charAt(l);
                l++;
                winMap.put(out, winMap.get(out) - 1);
            }
            max = Math.max(r - l, max);
        }
        return max == Integer.MIN_VALUE ? 0 : max;
    }
}

结尾

滑动窗口套路固定,遇到类型题时,直接模板默写代码,屡试不爽。 下篇文章讲二分搜索。

感谢各位人才的点赞、收藏和评论,干货文章持续更新中,下篇文章再见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341