算法 系列 滑动窗口

基础数据结构...

Posted by lichao modified on June 17, 2020

最小覆盖子串

algorithm

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

  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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
   static String minWindow(String s, String t) {
        // 记录最短⼦串的开始位置和⻓度
        int start = 0, minLen = Integer.MAX_VALUE;
        int left = 0, right = 0;
        Map<Character, Integer> window = new HashMap<>();
        Map<Character, Integer> needs = new HashMap<>();
        for (char c : t.toCharArray()) {
            needs.put(c, needs.getOrDefault(c, 0) + 1);
        }
        int match = 0;
        while (right < s.length()) {
            char c1 = s.charAt(right);
            if (needs.containsKey(c1)) {
                window.put(c1, window.getOrDefault(c1, 0) + 1);
                if (window.getOrDefault(c1, 0).equals(needs.getOrDefault(c1, 0)))
                    match++;
            }
            right ++;
            while (match == needs.size()) {
                if (right - left < minLen) {
                    // 更新最⼩⼦串的位置和⻓度
                    start = left;
                    minLen = right - left;
                }
                char c2 = s.charAt(left);
                if (needs.containsKey(c2)) {
                    window.put(c2, window.getOrDefault(c2, 0) - 1);
                    if (window.getOrDefault(c2, 0) < needs.getOrDefault(c2, 0))
                        match--;
                }
                left++;
            }
        }
        return minLen == Integer.MAX_VALUE ?
                "" : s.substring(start, start + minLen);
    }

这个算法的时间复杂度是 O(M + N),M 和 N 分别是字符串 S 和 T 的⻓度。因为先⽤ for 循环遍历了字符串 T 来初始化 needs,时间 O(N),之后的两个 while 循环最多执⾏ 2M 次,时间 O(M)。

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

algorithm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    static List<Integer> findAnagrams(String s, String t) {
        // ⽤数组记录答案
        List<Integer> res = new ArrayList<>();
        int left = 0, right = 0;
        Map<Character, Integer> needs = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();
        for (char c : t.toCharArray())
            needs.put(c, needs.getOrDefault(c, 0) + 1);
        int match = 0;
        while (right < s.length()) {
            char c1 = s.charAt(right);
            if (needs.containsKey(c1)) {
                window.put(c1, window.getOrDefault(c1, 0) + 1);
                if (window.get(c1).equals(needs.get(c1)))
                    match++;
            }
            right++;
            while (match == needs.size()) {
                // 如果 window 的⼤⼩合适
                // 就把起始索引 left 加⼊结果
                if (right - left == t.length()) {
                    res.add(left);
                }
                char c2 = s.charAt(left);
                if (needs.containsKey(c2)) {
                    window.put(c2, window.getOrDefault(c2, 0) - 1);
                    if (window.getOrDefault(c2, 0) < needs.getOrDefault(c2, 0))
                        match--;
                }
                left++;
            }
        }
        return res;
    }

⽆重复字符的最⻓⼦串

algorithm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    int lengthOfLongestSubstring(String s) {
        int left = 0, right = 0;
        Map<Character, Integer> window = new HashMap<>();
        int res = 0; // 记录最⻓⻓度
        while (right < s.length()) {
            char c1 = s.charAt(right);
            window.put(c1, window.getOrDefault(c1, 0) + 1);
            right++;
            // 如果 window 中出现重复字符
            // 开始移动 left 缩⼩窗⼝
            while (window.getOrDefault(c1, 0) > 1) {
                char c2 = s.charAt(left);
                window.put(c2, window.getOrDefault(c2, 0) - 1);
                left++;
            }
            res = Math.max(res, right - left);
        }
        return res;
    }

遇到⼦串问题,⾸先想到的就是滑动窗⼝技巧。

总结

可以总结出滑动窗⼝算法的抽象思想:

1
2
3
4
5
6
7
8
9
    int left = 0, right = 0;
    while (right < s.size()) {
        window.add(s[right]);
        right++;
        while (valid) {
            window.remove(s[left]);
            left++;
        }
    }

其中 window 的数据类型可以视具体情况⽽定,⽐如上述题⽬都使⽤哈希表充当计数器,当然也可以⽤⼀个数组实现同样效果,因为只处理英⽂字⺟。