LeetCode - 76. 最小覆盖子串

题目

76. 最小覆盖子串 - 力扣(LeetCode)

假设我们用最简单的滑动窗口思路:设置左右指针,然后移动右指针扩大窗口,直到窗口包含所有 t 中的字符,再移动左指针缩小窗口。但是我们会发现一些问题

无法准确判断"窗口包含了t的所有字符"

  • 例如,如果t="ABC",窗口="ABCB",虽然包含ABC,但我们不能只检查字符是否存在。

重复字符的处理

  • 例如,如果t="AAB",窗口="AAC",虽然包含A和A,但缺少B。

我们需要增加条件,不仅关注字符是否出现,还要关注出现的次数,使用哈希表记录t中每个字符需要的次数

完整思路

1. 预处理阶段

  • 创建两个哈希表:need 记录目标字符串 t 中每个字符需要的数量,window 记录当前窗口中每个字符的数量。
  • 遍历字符串 t,统计每个字符出现的次数,存入 need 哈希表。
  • 初始化左右指针 left 和 right 为 0,表示窗口的边界。
  • 初始化 valid 变量为 0,用于记录当前窗口中满足条件的字符种类数。
  • 初始化结果相关变量:start 为子串起始位置,len 为子串长度,初始化为一个很大的值。

2. 扩展窗口阶段

  • 移动右指针,将字符纳入窗口。
  • 如果当前字符是目标字符(即在 need 中),更新 window 中该字符的计数。
  • 如果该字符在窗口中的数量恰好等于需要的数量,则 valid 加 1。

3. 收缩窗口阶段

  • 当 valid 等于 need 中字符种类数时,说明窗口已包含 t 中所有字符且数量都满足。
  • 尝试收缩窗口,移动左指针,在保证窗口仍然满足条件的情况下找到最小长度的子串。
  • 每次左指针移动前,更新最小子串信息(如果当前窗口长度小于已知最小长度)。
  • 如果左指针指向的字符在 need 中,且在 window 中的数量等于需要的数量,则移除后会导致窗口不再满足条件,需要将 valid 减 1。
  • 更新 window 中对应字符的计数。

4. 重复循环

  • 重复步骤 2 和 3,直到右指针到达字符串 s 的末尾。

5. 结果处理

  • 如果找到了满足条件的子串,返回该子串;否则返回空字符串。
  • 注意处理边界情况,如 t 为空字符串,s 为空字符串等。

时间复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 s 的长度。
  • 空间复杂度:O(k),其中 k 是字符集大小,最坏情况下为 O(|\Sigma|),|\Sigma| 是字符集大小。

过程

以例子 s = "ADOBECODEBANC", t = "ABC" 进行说明。

初始化

  • need = {A:1, B:1, C:1} (t中每个字符需要的数量)
  • window = {} (当前窗口中每个字符的数量)
  • left = 0, right = 0 (窗口的左右边界)
  • valid = 0 (已满足条件的字符数)
  • start = 0, len = INT_MAX (最小子串的起始位置和长度)

滑动窗口过程

扩展窗口阶段:

右指针移动到0: s[0] = 'A'

  • 加入窗口: window = {A:1}
  • 'A'在need中,且数量满足: valid = 1
  • 窗口: [A],valid = 1,不满足条件

右指针移动到1: s[1] = 'D'

  • 加入窗口: window = {A:1, D:1}
  • 'D'不在need中: valid不变
  • 窗口: [AD],valid = 1,不满足条件

右指针移动到2: s[2] = 'O'

  • 加入窗口: window = {A:1, D:1, O:1}
  • 'O'不在need中: valid不变
  • 窗口: [ADO],valid = 1,不满足条件

右指针移动到3: s[3] = 'B'

  • 加入窗口: window = {A:1, D:1, O:1, B:1}
  • 'B'在need中,且数量满足: valid = 2
  • 窗口: [ADOB],valid = 2,不满足条件

右指针移动到4: s[4] = 'E'

  • 加入窗口: window = {A:1, D:1, O:1, B:1, E:1}
  • 'E'不在need中: valid不变
  • 窗口: [ADOBE],valid = 2,不满足条件

右指针移动到5: s[5] = 'C'

  • 加入窗口: window = {A:1, D:1, O:1, B:1, E:1, C:1}
  • 'C'在need中,且数量满足: valid = 3
  • 窗口: [ADOBEC],valid = 3,满足条件

收缩窗口阶段:

满足条件,开始收缩,更新最小值: len = 6, start = 0

左指针移动到1: s[0] = 'A'移出窗口

  • 更新窗口: window = {A:0, D:1, O:1, B:1, E:1, C:1}
  • 'A'在need中,且移除后不满足: valid = 2
  • 窗口: [DOBEC],valid = 2,不满足条件,停止收缩

继续扩展窗口:

右指针移动到6: s[6] = 'O'

  • 加入窗口: window = {A:0, D:1, O:2, B:1, E:1, C:1}
  • 'O'不在need中: valid不变
  • 窗口: [DOBECO],valid = 2,不满足条件

右指针移动到7: s[7] = 'D'

  • 加入窗口: window = {A:0, D:2, O:2, B:1, E:1, C:1}
  • 'D'不在need中: valid不变
  • 窗口: [DOBECOD],valid = 2,不满足条件

右指针移动到8: s[8] = 'E'

  • 加入窗口: window = {A:0, D:2, O:2, B:1, E:2, C:1}
  • 'E'不在need中: valid不变
  • 窗口: [DOBECODE],valid = 2,不满足条件

右指针移动到9: s[9] = 'B'

  • 加入窗口: window = {A:0, D:2, O:2, B:2, E:2, C:1}
  • 'B'在need中,但之前已满足: valid不变
  • 窗口: [DOBECODEB],valid = 2,不满足条件

右指针移动到10: s[10] = 'A'

  • 加入窗口: window = {A:1, D:2, O:2, B:2, E:2, C:1}
  • 'A'在need中,且数量满足: valid = 3
  • 窗口: [DOBECODEBA],valid = 3,满足条件

再次收缩窗口:

满足条件,开始收缩

左指针移动到1: s[0] = 'D'移出窗口

  • 更新窗口: window = {A:1, D:1, O:2, B:2, E:2, C:1}
  • 'D'不在need中: valid不变
  • 窗口: [OBECODEBA],valid = 3,仍满足条件

左指针继续移动:

  • 移出'O','B','E','C','O','D','E'
  • 当移出'C'时,valid变为2,不满足条件,停止收缩
  • 此时窗口长度为4,更新 len = 4, start = 9
  • 窗口: [BANC],valid = 3,满足条件

最终结果

  • 最小覆盖子串: s.substr(start, len) = "BANC"

为啥最后返回的是,s.substr(start, len); 不是数据已经保存在窗口中了嘛 直接返回窗口中的数据不就好了

原因如下:

window 哈希表不保存完整字符串:

  • window 哈希表只记录了各个字符及其出现的次数,例如 {'a': 2, 'b': 1}
  • 它不保存字符的顺序或完整的子串

窗口可能包含非必要字符:

  • 最小窗口可能包含一些不在字符串 t 中的字符
  • 这些字符在 window 中可能有记录,但不是所有 window 中的字符都需要

需要保持原始顺序:

  • 子串必须保持原始字符串 s 中的字符顺序
  • 从哈希表重建字符串会丢失顺序信息

例如,对于输入 s = "ADOBECODEBANC", t = "ABC":

  • 最小窗口是 "BANC"
  • window 哈希表可能包含 {'B':1, 'A':1, 'N':1, 'C':1}
  • 但从这个哈希表无法重建 "BANC",因为我们不知道字符的顺序

因此,正确的做法是记录最小窗口的起始位置 start 和长度 len,然后使用 s.substr(start, len) 从原字符串中截取出这个子串。这样可以保证返回的是原始顺序的完整子串。

读者可能出现的错误写法

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) 
    {
        unordered_map<char,int> need;
        for(char e : t)
        {
            need[e]++;
        }

        int left = 0;
        int right = 0;
        int len = 0;
        int start = 0;

        int valid = 0;

        unordered_map<char,int> window;

        while(right < s.size())
        {
            window[s[right]]++;

            if(window[s[right]] == need[s[right]])
            {
                valid++;
            }

            right++;

            while(valid == need.size())
            {
                start = left;
                len = min(len,right-left);

                window[s[left]]--;

                if(window[s[left]] == need[s[left]])
                {
                    valid--;
                }
                left++;
            }
        }
        return len==INT_MAX ? "" : s.substr(start,right);
    }
};

语法错误:

cpp 复制代码
   if(window[s[right]] == need[s[right]])

cpp 复制代码
   if(window[s[left]] == need[s[left]])

当 s[right] 或 s[left] 不在 need 哈希表中时,直接访问 need[s[right]] 会创建一个默认值为0的新键。你应该先检查字符是否在 need 中。

初始化错误:

  • len 初始化为 0,应该初始化为 INT_MAX,因为我们要找最小窗口
  • 使用 min(len, right-left) 时,如果 len=0,永远不会更新

返回值错误:

  • 返回语句中用的是 s.substr(start, right),第二个参数应该是长度 len,而不是 right

条件检查不足:

  • 需要先检查字符是否在 need 中,再进行比较

正确解法

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) 
    {
        unordered_map<char,int> need;
        for(char e : t)
        {
            need[e]++;
        }

        int left = 0;
        int right = 0;
        int len = INT_MAX;
        int start = 0;

        int valid = 0;

        unordered_map<char,int> window;

        while(right < s.size())
        {
            char r = s[right];
            right++;

            if(need.count(r))
            {
                window[r]++;
                if(window[r] == need[r])
                {
                    valid++;
                }
            }

            while(valid == need.size())
            {
                if(len > right-left)
                {
                    start = left;
                    len = right-left;
                }

                char l = s[left];
                left++;
                if(need.count(l))
                {
                    if(window[l] == need[l])
                    {
                        valid--;
                    }
                    window[l]--;
                }

            }
        }
        return len==INT_MAX ? "" : s.substr(start,len);
    }
};
相关推荐
2301_764441333 小时前
LISA时空跃迁分析,地理时空分析
数据结构·python·算法
东北洗浴王子讲AI3 小时前
GPT-5.4辅助算法设计与优化:从理论到实践的系统方法
人工智能·gpt·算法·chatgpt
问简4 小时前
docker 镜像相关
运维·docker·容器
Billlly4 小时前
ABC 453 个人题解
算法·题解·atcoder
玉树临风ives4 小时前
atcoder ABC 452 题解
数据结构·算法
Dream of maid4 小时前
Linux(下)
linux·运维·服务器
齐鲁大虾4 小时前
统信系统UOS常用命令集
linux·运维·服务器
feifeigo1235 小时前
基于马尔可夫随机场模型的SAR图像变化检测源码实现
算法
Benszen5 小时前
Docker容器化技术实战指南
运维·docker·容器
ZzzZZzzzZZZzzzz…5 小时前
Nginx 平滑升级:从 1.26.3 到 1.28.0,用户无感知
linux·运维·nginx·平滑升级·nginx1.26.3·nginx1.28.0