(12)LeetCode 76. 最小覆盖子串

LeetCode 76. 最小覆盖子串(滑动窗口详解)

📌 题目描述

给你两个字符串 st,长度分别为 mn。请你返回 s 中最短的子串 ,使得该子串包含 t 中的所有字符(包括重复字符 )。如果不存在这样的子串,则返回空字符串 ""

注意:

  • 测试用例保证答案唯一。
  • 如果 s 中没有覆盖 t 的子串,返回 ""

示例 1:

复制代码
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最短覆盖子串是 "BANC"(长度为3),虽然 "ADOBEC" 也包含 ABC,但更长。

示例 2:

复制代码
输入:s = "a", t = "a"
输出:"a"

示例 3:

复制代码
输入:s = "a", t = "aa"
输出:""
解释:t 中两个 'a' 都需要被覆盖,但 s 只有一个,故返回 ""。

💡 解题思路

本题是经典的 滑动窗口 问题,可以使用 双指针 + 哈希表 在 O ( m ) O(m) O(m) 时间内解决。

核心思想:

  1. 用哈希表 ht 记录目标字符串 t 中每个字符的出现次数(即"需求量")。
  2. 维护一个滑动窗口 [j, i] ,其中 i 是右指针,j 是左指针。
  3. 用另一个哈希表 hs 记录当前窗口内字符的实际出现次数
  4. 引入一个计数器 cnt ,记录当前窗口中"有效匹配"的字符个数(即窗口中已经满足 t 需求的字符数量)。
  5. 扩展右指针 ,每次加入新字符,若该字符在窗口中的数量不超过需求量,则 cnt++
  6. 收缩左指针 ,当窗口包含所有 t 的字符后(即 cnt == t.size()),尝试向右移动左指针,去掉冗余字符,以缩小窗口长度。
  7. 在收缩过程中更新最小窗口

🧩 代码实现(C++)

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> hs, ht;   // hs: 窗口频次,ht: 目标频次
        for (char c : t) ht[c]++;          // 统计 t 中每个字符的需用量

        string res;                        // 最终答案
        int cnt = 0;                       // 窗口中有效匹配的字符个数

        for (int i = 0, j = 0; i < s.size(); i++) {
            // 1. 扩展右边界,将 s[i] 加入窗口
            hs[s[i]]++;
            if (hs[s[i]] <= ht[s[i]]) cnt++;   // 若该字符的数量未超过需求量,则有效计数+1

            // 2. 收缩左边界:移除窗口左侧所有冗余字符
            while (hs[s[j]] > ht[s[j]]) {
                hs[s[j]]--;   // 移除左边界字符
                j++;          // 左指针右移
            }

            // 3. 如果窗口已经覆盖 t,尝试更新答案
            if (cnt == t.size()) {
                int curLen = i - j + 1;
                if (res.empty() || curLen < res.size()) {
                    res = s.substr(j, curLen);
                }
            }
        }
        return res;
    }
};

🔍 代码逐行详解

1️⃣ 哈希表初始化

cpp 复制代码
unordered_map<char, int> hs, ht;
for (char c : t) ht[c]++;
  • ht 存储了 t 中每个字符的需求次数,例如 t = "AAB",则 ht['A']=2, ht['B']=1
  • hs 初始为空,用于动态记录当前窗口 [j, i] 内字符出现的次数。

2️⃣ 遍历右指针 i

cpp 复制代码
for (int i = 0, j = 0; i < s.size(); i++) {
    hs[s[i]]++;
    if (hs[s[i]] <= ht[s[i]]) cnt++;
    ...
}
  • 每次将 s[i] 纳入窗口,先更新 hs
  • 如果该字符在窗口中的数量 没有超过 它在 t 中的需求量,说明这个字符是 有效 的,cnt++
    例如,t 中需要 2 个 A,窗口已有 1 个 A,再来一个 A 时,hs['A']=2,满足 <= ht['A'],因此 cnt++。若再来第三个 Ahs['A']=3 > 2,则不会增加 cnt

3️⃣ 收缩左指针 j

cpp 复制代码
while (hs[s[j]] > ht[s[j]]) {
    hs[s[j]]--;
    j++;
}
  • 当窗口已包含所需所有字符时(cnt == t.size()),我们需要 尽可能缩短窗口
  • 检查左指针 j 指向的字符是否 冗余(即窗口中的数量超过了需求量)。
  • 如果是,则将其移出窗口(频次减1,左指针右移),直到左边界字符不再冗余为止。
  • 此步骤保证了窗口在满足覆盖的前提下,左边界无法再向右移动,即当前窗口是 i 为右边界时的最小窗口

4️⃣ 更新答案

cpp 复制代码
if (cnt == t.size()) {
    if (res.empty() || i - j + 1 < res.size())
        res = s.substr(j, i - j + 1);
}
  • cnt == t.size() 时,表示窗口已经覆盖了 t 的全部字符。
  • 此时比较当前窗口长度是否小于已记录的最短长度,若是则更新 res

🎯 为什么 cnt == t.size() 能保证覆盖?

  • cnt 统计的是窗口中 有效字符的总数 ,每个有效字符意味着该字符在窗口中的出现次数没有超过它在 t 中的需求量。
  • cnt == t.size() 时,说明窗口中已经累积了恰好等于 t 长度的有效字符,且由于收缩步骤保证了任意字符均不超过需求量,因此 窗口内每个字符的数量正好等于(或大于等于,但冗余已被移除)需求 ,即完全覆盖 t

📊 复杂度分析

  • 时间复杂度 : O ( m ) O(m) O(m),其中 m m m 是字符串 s 的长度。
    左右指针均只会向右移动,每个字符最多被加入和移出窗口各一次,因此总操作次数为 O ( m ) O(m) O(m)。
  • 空间复杂度 : O ( ∣ Σ ∣ ) O(|\Sigma|) O(∣Σ∣),其中 ∣ Σ ∣ |\Sigma| ∣Σ∣ 是字符集大小(本题通常为 52 个英文字母,或扩展为 256 个 ASCII 字符)。哈希表存储的字符种类有限,可视为 O ( 1 ) O(1) O(1)。

🧪 测试用例验证

输入 输出 解释
s = "ADOBECODEBANC", t = "ABC" "BANC" 最短覆盖子串
s = "a", t = "a" "a" 单字符匹配
s = "a", t = "aa" "" 无法覆盖两个 'a'
s = "aa", t = "aa" "aa" 必须完全匹配两个 'a'
s = "bbaa", t = "aba" "baa" 注意顺序不影响,子串包含即可

💭 总结与扩展

  • 滑动窗口是解决子串/子数组问题的重要技巧,常用于求满足条件的最短/最长区间。
  • 本题的 核心难点 在于如何高效判断窗口是否覆盖 t,以及如何处理重复字符。
  • 使用 双哈希表 + 计数器 是解决此类问题的经典范式,掌握后可轻松应对 LeetCode 438、567 等相似题目。
相关推荐
灯澜忆梦1 小时前
GO_函数_1
算法
旧曲重听12 小时前
为什么现在 RAG 越少越少提及了
数据库·程序人生·职场和发展·agent
言乐62 小时前
Python实现建造微服务商城后台
开发语言·python·算法·微服务·架构
凉云生烟2 小时前
机器学习 02- KNN算法
人工智能·算法·机器学习
wabs6662 小时前
关于动态规划【力扣583.两个字符串的删除操作的思考】
算法·leetcode·动态规划
可编程芯片开发2 小时前
空气流量和空气压力参数解耦系统simulink建模与仿真
算法
_Doubletful3 小时前
妙用位运算:解构汉明距离至100%(提供分析与多解)
c语言·算法·leetcode
凯瑟琳.奥古斯特3 小时前
力扣1013三等分解法与C++实现
开发语言·c++·算法·leetcode·职场和发展
Keven_113 小时前
AcWing算法提高课思路速查:搜索
算法·搜索