滑动窗口模板在字符串中的巧妙应用|LeetCode 76 最小覆盖子串

LeetCode 76 最小覆盖子串

点此看全部题解 LeetCode必刷100题:一份来自面试官的算法地图(题解持续更新中)
更多干货,请关注公众号【忍者算法】,回复【刷题清单】获取完整题解目录~

从生活中理解这个问题

想象你是一位珠宝设计师,要用一段项链(可能包含各种宝石)找出最短的一段,这段中必须包含顾客指定的所有种类的宝石。比如顾客要求必须有红宝石、蓝宝石和钻石,你需要找出包含这三种宝石的最短项链片段。

这就是"最小覆盖子串"问题的生活映射:在一个长字符串中找到包含目标字符串所有字符的最短子串。

问题描述

LeetCode第76题是这样描述的:给你一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字符的最小子串。

举个例子:

S = "ADOBECODEBANC"
T = "ABC"
输出: "BANC"

解释:我们要找的子串必须包含A、B和C,而"BANC"是所有满足条件的子串中最短的。

解题套路:滑动窗口模板的应用

滑动窗口是一类特殊的双指针技巧,特别适合处理子串、子数组的问题。我们先来理解这个通用模板。

滑动窗口通用模板

// 通用的滑动窗口模板
public String slidingWindowTemplate(String s, String t) {
    // 1. 定义窗口内的数据结构(通常是HashMap或数组)
    Map<Character, Integer> window = new HashMap<>();
    Map<Character, Integer> need = new HashMap<>();
    
    // 2. 初始化目标条件
    for (char c : t.toCharArray()) {
        need.put(c, need.getOrDefault(c, 0) + 1);
    }
    
    // 3. 定义窗口的左右边界和控制变量
    int left = 0, right = 0;
    int valid = 0;  // 用于判断条件是否满足
    
    // 4. 循环扩大窗口
    while (right < s.length()) {
        // 5. 取出将要加入窗口的字符
        char c = s.charAt(right);
        right++;  // 扩大窗口
        
        // 6. 进行窗口内数据的一系列更新
        ...
            
        // 7. 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // 8. 取出将要移出窗口的字符
            char d = s.charAt(left);
            left++;  // 缩小窗口
            
            // 9. 进行窗口内数据的一系列更新
            ...
        }
    }
    return 最终结果;
}

这个模板的精髓在于:

  1. 两个指针:控制窗口的左右边界
  2. 数据结构:维护窗口内的状态
  3. 双层循环:外层扩展右边界,内层收缩左边界
  4. 更新规则:清晰的窗口数据更新规则

运用模板解决最小覆盖子串

现在让我们用这个模板来解决我们的问题:

public String minWindow(String s, String t) {
    // 记录目标字符串中每个字符出现的次数
    Map<Character, Integer> need = new HashMap<>();
    for (char c : t.toCharArray()) {
        need.put(c, need.getOrDefault(c, 0) + 1);
    }
    
    // 记录窗口中的字符及其出现次数
    Map<Character, Integer> window = new HashMap<>();
    
    int left = 0, right = 0;  // 窗口的左右边界
    int valid = 0;    // 已经匹配的字符个数
    
    // 记录最小覆盖子串的起始位置和长度
    int start = 0, minLen = Integer.MAX_VALUE;
    
    while (right < s.length()) {
        // 扩大窗口
        char c = s.charAt(right);
        right++;
        
        // 如果是需要的字符,更新窗口数据
        if (need.containsKey(c)) {
            window.put(c, window.getOrDefault(c, 0) + 1);
            // 如果这个字符的数量正好匹配需求,valid加1
            if (window.get(c).equals(need.get(c))) {
                valid++;
            }
        }
        
        // 当所有字符都匹配后,尝试缩小窗口
        while (valid == need.size()) {
            // 更新最小覆盖子串
            if (right - left < minLen) {
                start = left;
                minLen = right - left;
            }
            
            // 缩小窗口
            char d = s.charAt(left);
            left++;
            
            // 如果移出的是需要的字符,更新窗口数据
            if (need.containsKey(d)) {
                if (window.get(d).equals(need.get(d))) {
                    valid--;
                }
                window.put(d, window.get(d) - 1);
            }
        }
    }
    
    return minLen == Integer.MAX_VALUE ? "" 
           : s.substring(start, start + minLen);
}

让我们用一个简单的例子来详细演示这个过程:

S = "ADOBEC"
T = "ABC"

1. 初始状态:
need = {A:1, B:1, C:1}
window = {}
valid = 0

2. 遇到'A'window = {A:1}
valid = 1  // A达到要求

3. 遇到'D':
不是需要的字符,跳过

4. 遇到'O':
不是需要的字符,跳过

5. 遇到'B'window = {A:1, B:1}
valid = 2  // B达到要求

6. 遇到'E':
不是需要的字符,跳过

7. 遇到'C'window = {A:1, B:1, C:1}
valid = 3  // 所有字符都达到要求了

8. 开始收缩窗口...

滑动窗口解题模板的四个重点

  1. 窗口定义

    • 明确窗口应该包含什么
    • 明确什么时候扩大窗口
    • 明确什么时候缩小窗口
  2. 状态变量

    • 使用合适的数据结构记录状态
    • 明确状态的更新规则
    • 明确有效状态的判断条件
  3. 更新规则

    • 扩大窗口时如何更新
    • 缩小窗口时如何更新
    • 什么时候更新结果
  4. 边界条件

    • 初始化值的设置
    • 结果不存在的处理
    • 特殊情况的考虑

类似题目及解题思路

这个模板可以解决很多类似的问题:

  • 字符串的排列
  • 找到字符串中所有字母异位词
  • 无重复字符的最长子串

解决这类问题的通用步骤:

  1. 确定是否适合用滑动窗口
  2. 定义窗口的意义
  3. 确定状态变量和更新规则
  4. 套用模板编写代码
  5. 处理边界条件

小结

掌握滑动窗口模板,就像学会了一把万能钥匙,可以解开许多字符串子串问题的大门。记住:

  1. 模板的核心是状态的维护和更新
  2. 左右指针的移动要有明确的逻辑
  3. 条件的判断要准确无误

下次遇到子串相关的问题,不妨先想想是否可以用这个模板来解决!


作者:忍者算法
公众号:忍者算法

posted @   忍者算法  阅读(17)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签
点击右上角即可分享
微信分享提示