力扣76.最小覆盖子串

149 阅读4分钟

题目描述:

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'

示例 2:

输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。

示例 3:

输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 105
  • s 和 t 由英文字母组成

实现代码

class Solution {
public:
    static bool check(const unordered_map<char,int>& need_map,unordered_map<char,int>& window_map) {//检查是否覆盖了
        for(auto i:need_map) {
            if(i.second>window_map[i.first]) {
                return false;
            }
        }
        return true;
    }
    string minWindow(string s, string t) {
        bool flag=0;
        unordered_map<char,int> need_map;
        unordered_map<char,int> window_map;
        for(auto i: t) {
            need_map[i]++;
        }
        int left=0,right=0,min=INT_MAX,minLeft=0,minRight=0;
        while(right<s.size()) {//不断向右移动
            window_map[s[right]]++;//更新窗口
            if(check(need_map,window_map)) {//如果足够包含了
                flag=1;
                window_map[s[left++]]--;//尝试左指针右移
                while(check(need_map,window_map)) {//直到找到不符合的那个左指针
                    window_map[s[left++]]--;
                }
                if(right-left+1<min) {//左指针会多移动一格
                    min=right-left+1;
                    minRight=right;
                    minLeft=left-1;
                }
            }
            right++;
        }
        string ans;
        if(flag==0){
            return "";
        }
        for(int i=minLeft;i<=minRight;i++) {
            ans.push_back(s[i]);
        }
        return ans;
    }
};

1. 核心逻辑

通过滑动窗口算法实现最小覆盖子串:

  • 窗口扩展:通过 right 指针不断扩展窗口,直到窗口包含了 t 中所有字符。
  • 窗口收缩:移动 left 指针尝试缩小窗口,直到窗口无法再满足条件为止。
  • 记录结果:每次收缩窗口后,如果当前窗口大小比之前的最优结果更小,则更新最优结果。

2. 数据结构

  • need_map: 一个哈希表,用于记录 t 中每个字符需要的次数。
  • window_map: 一个哈希表,用于记录当前窗口中每个字符的次数。
  • 辅助函数 check: 比较 window_mapneed_map,判断当前窗口是否覆盖了 t

3. 关键变量

  • leftright: 滑动窗口的左右指针。
  • min: 当前最小窗口长度。
  • minLeftminRight: 记录最优窗口的起点和终点。
  • flag: 用于判断是否找到满足条件的子串。

4. 核心函数解析

check 函数

用于检查当前窗口是否满足条件:

static bool check(const unordered_map<char,int>& need_map,unordered_map<char,int>& window_map) {
    for(auto i: need_map) {
        if(i.second > window_map[i.first]) { // 当前窗口中某字符频次不足
            return false;
        }
    }
    return true; // 所有字符频次都满足
}

minWindow 函数

主函数,负责滑动窗口的逻辑。

步骤:

  1. 初始化数据结构

    • 构建 need_map,记录 t 中每个字符及其频次。
    • 初始化滑动窗口左右指针 leftright
  2. 扩展窗口

    while(right < s.size()) {
        window_map[s[right]]++; // 将当前字符加入窗口
    
  3. 检查窗口有效性

    if(check(need_map, window_map)) { // 当前窗口覆盖了所有需要的字符
    
  4. 收缩窗口

    while(check(need_map, window_map)) { // 左指针右移,尽量缩小窗口
        window_map[s[left++]]--;
    
  5. 更新最优解

    if(right - left + 1 < min) { // 更新最小窗口
        min = right - left + 1;
        minRight = right;
        minLeft = left - 1; // 左指针多移动了一格
    
  6. 最终结果: 如果 flag0,说明未找到符合条件的子串;否则返回最优窗口。


5. 示例运行

输入:

s = "ADOBECODEBANC"
t = "ABC"

运行过程

  1. 初始化:need_map = {A:1, B:1, C:1}
  2. 滑动窗口扩展:右指针逐步移动,将字符加入 window_map
  3. 检查条件并收缩:在 window_map 满足条件后,左指针右移,缩小窗口。
  4. 更新最优解:每次找到更小的窗口时更新 minLeftminRight
  5. 最终返回:"BANC"

6. 时间复杂度分析

  • 时间复杂度: (O(n + m))

    • (n) 是字符串 s 的长度,滑动窗口的扩展和收缩最多执行 (n) 次。
    • (m) 是字符串 t 的长度,用于初始化 need_map
  • 空间复杂度: (O(m)),need_mapwindow_map 需要存储 t 中的字符及其频次。


流程图:

flowchart TD
    A[输入字符串 s 和 t] --> B[初始化 need_map 和 window_map]
    B --> C[右指针向右扩展]
    C --> D[更新 window_map]
    D --> E{check: 是否覆盖了 t}
    E -- 是 --> F[尝试移动左指针收缩窗口]
    F --> G{check: 窗口仍满足条件吗?}
    G -- 是 --> H[继续收缩窗口]
    G -- 否 --> I[更新最小窗口结果]
    E -- 否 --> C
    I --> J{右指针到达末尾了吗?}
    J -- 否 --> C
    J -- 是 --> K{是否找到有效窗口?}
    K -- 是 --> L[返回最小窗口子串]
    K -- 否 --> M[返回空字符串]

关键节点说明

  1. 初始化阶段

    • need_map 保存 t 中每个字符的频次。
    • window_map 用于记录滑动窗口中每个字符的频次。
  2. 扩展右指针

    • 每次移动右指针时,将对应字符加入 window_map
  3. 检查覆盖条件

    • 调用 check 函数,判断当前窗口是否包含 t 所有字符及其频次。
  4. 收缩左指针

    • 如果窗口满足条件,则移动左指针尝试缩小窗口,同时更新 window_map
  5. 更新最优解

    • 如果当前窗口长度更小,则记录当前窗口的起点和长度。
  6. 结束判断

    • 当右指针到达末尾,判断是否找到满足条件的子串,返回结果。