【算法】滑动窗口—最小覆盖子串

题目

"最小覆盖子串"问题,难度为Hard,题目如下:

给你两个字符串 S 和 T,请你在 S 中找到包含 T 中全部字母的最短子串。如果 S 中没有这样一个子串,则算法返回空串,如果存在这样一个子串,则可以认为答案是唯一的。

比如输入 S = "ADBECFEBANC",T = "ABC",算法应该返回 "BANC"。

如果我们使用暴力解法,代码大概是这样的:

for (int i = 0; i < s.size; i++) {

for (int j = i + 1; j < s.size; j++) {

if i : j 包含 t 的所有字母:

更新答案

}

}

思路很简单,但显然不是我们想要的。

滑动窗口思路分析

1.我们在字符串 S 中使用双指针中的左、右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个"窗口"。

2.先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

3.此时,停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中所有字符了)。同时,每次增加 left,都要更新一轮结果。

4.重复第2和第3步,直到 right 到达字符 S 的尽头。

第2步相当于在寻找一个"可行解",然后第3步在优化这个"可行解",最终找到最优解,也就是最短的覆盖子串。左、右指针轮流前进 ,窗口大小增增减减,窗口不断向右滑动,这就是"滑动窗口"这个名字的来历。

下面画图理解一下这个思路。needs 和 window 相当于计数器,分别记录 T 中字符出现次数和"窗口"中的相应字符的出现次数。

初始状态:

增加 right,直到窗口 [left, right) 包含了 T 中所有字符:

现在开始增加 left,缩小窗口 [left, right):

直到窗口中的字符串不再符合要求,left 不再继续移动:

之后重复上述过程,先移动 right,再移动 left······直到 right 指针到达字符串 S 的末端,算法结束。现在来看看滑动窗口代码框架怎么用。

首先,初始化 window 和 need 两个哈希表,记录窗口中的字符和需要凑齐的字符:

Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
char key = t.charAt(i);
need.put(key, need.getOrDefault(key, 0) + 1);
}

然后,使用 left 和 right 变量初始化窗口的两端,不要忘了,区间 [left, right) 是左闭右开的,所以初始情况下窗口没有包含任何元素:

int left = 0, right = 0, valid = 0;
while (right < s.length()) { // 开始滑动 }

其中,valid 变量表示窗口中满足 need 条件的字符个数,如果 valid 和 need.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T。

现在开始套模板,只需要思考以下4个问题:

1.当移动 right 扩大窗口,即加入字符时,应该更新哪些数据?

2.什么条件下,窗口应该暂停扩大,开始移动 left 缩小窗口?

3.当移动 left 缩小窗口,即移出字符时,应该更新哪些数据?

4.我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

一般来说,如果一个字符进入窗口,应该增加 window 计数器;如果一个字符移出窗口,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;收缩窗口的时候应该更新最终结果。

下面是完整代码:

java 复制代码
package SlidingWindow;

import java.util.HashMap;
import java.util.Map;

// leetcode 017 最小覆盖子串
public class MCS {

    public String slidingWindow(String s, String t) {
        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();
        for (int i = 0; i < t.length(); i++) {
            char key = t.charAt(i);
            need.put(key, need.getOrDefault(key, 0) + 1);
        }
        int left = 0, right = 0, valid = 0; // valid 表示窗口中满足 need 条件的字符个数
        // 记录最小覆盖子串的启始索引及长度
        int start = 0, len = Integer.MAX_VALUE;
        while (right < s.length()) {
            // c 是将要移入窗口的字符
            char c = s.charAt(right);
            // 右移窗口
            right++;
            // 进行窗口内数据的一系列更新
            if (need.containsKey(c)) {
                window.put(c, window.getOrDefault(c, 0) + 1);
                if (window.getOrDefault(c, 0).equals(need.getOrDefault(c, 0))) { // window[c] == need[c]
                    valid++;
                }
            }

            /*** debug 输出的位置***/
            System.out.println("window:(" + left + ", " + right + ")");
            /*********************/

            // 判断左侧窗口是否要收缩
            while (valid == need.size()) { // window need shrink ---窗口需要收缩
                // 在这里更新最小覆盖子串
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                // d 是将要移出窗口的字符
                char d = s.charAt(left);
                // 左移窗口
                left++;
                // 进行窗口内数据的一系列更新
                if (need.containsKey(d)) {
                    if (window.getOrDefault(d, 0).equals(need.getOrDefault(d, 0))) {
                        valid--;
                    }
                    window.put(d, window.getOrDefault(d, 0) - 1);
                }
            }
        }
        // 返回最小覆盖子串
        return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len); // s.substring(start, start + len) == C++ 中的 s.substr(start, len)
    }

    public static void main(String[] args) {
        MCS mcs = new MCS();
        String str = mcs.slidingWindow("ADOBECODEBANC", "ABC");
        System.out.println(str);
    }
    
}
相关推荐
Jerry10 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry12 小时前
LeetCode 459. 重复的子字符串
算法
海石15 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石15 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry17 小时前
LeetCode 151. 反转字符串中的单词
算法
a11177619 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜20 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry20 小时前
LeetCode 541. 反转字符串 II
算法
Jerry20 小时前
LeetCode 344. 反转字符串
算法
搞科研的小刘选手20 小时前
【香港大学主办&IEEE出版】第六届计算机视觉、应用与算法国际学术会议(CVAA 2026)
算法·计算机视觉·应用·学术会议