AlgoMooc
你已开通华为OD训练营权益,还差最后一步——完成入营激活(兑换课程 + 加飞书 + 登记服务群),即可解锁全部课程与专属服务。去激活 →
← OD 20 天课程

华为 OD 训练营 · 题解精讲

LC76. 最小覆盖子串

配套讲解:视频讲解

LC76. 最小覆盖子串 视频地址:https://uha.xet.tech/s/2xvb3S

题目描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。 注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 如果 s 中存在这样的子串,我们保证它是唯一的答案。 示例 1: 输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 示例 2: 输入:s = "a", t = "a" 输出:"a" 示例 3: 输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。 提示: 1 <= s.length, t.length <= 105 s 和 t 由英文字母组成 进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?

题目解析

解题思路

本题使用滑动窗口算法,在字符串 s 上维护一个动态窗口,通过移动左右指针寻找包含 t 所有字符的最短子串。

核心数据结构:使用哈希表(defaultdict(int))记录当前窗口中各字符的“需求差额”。初始时,将 t 中每个字符计数为正数,表示还需要多少个该字符。count 变量记录 t 中尚未被满足的字符总数。

算法流程: 1. 右指针 right 不断向右扩展窗口,每遇到一个字符 c,将其在 map 中的计数减 1。若减之前该字符计数 > 0,说明该字符是 t 中需要的,count 减 1。 2. 当 count == 0 时,当前窗口已包含 t 所有字符。此时尝试收缩左边界:若左指针字符在 map 中的计数为 0,说明该字符是“关键字符”,移除后 count 加 1;否则是多余字符,直接移除。每次移除后左指针右移,并更新最小窗口长度和起始位置。 3. 重复直到右指针遍历完整个 s

关键步骤


初始: s = "ADOBECODEBANC", t = "ABC"
map: {'A':1, 'B':1, 'C':1}, count=3

右指针移动到 'C' 时: map: {'A':0, 'B':0, 'C':0, ...}, count=0
窗口 "ADOBEC" 满足条件,尝试收缩左边界:
  左指针 'A'map 中为 0 → 关键字符,移除后 count=1
  左指针右移,窗口变为 "DOBEC"
此时 count≠0,继续右移右指针...
最终找到最短窗口 "BANC"

复杂度分析

  • 时间复杂度:O(n),其中 n 为 s 的长度。左右指针各遍历 s 一次,每个字符最多被访问两次(一次加入窗口,一次移出窗口)。
  • 空间复杂度:O(1)。map 数组大小固定为 128(ASCII 字符集),不随输入规模变化。

参考代码

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        # 使用整型数组来表示每个字符在 t 中的数量,初始化都为 0,利用 ASCII 码的方式把字符存储到整型数组中
        # 数组的长度设置为 128
        # 其中 65 ~ 90 号为 26 个大写英文字母,97 ~ 122 号为 26 个小写英文字母,其余为一些标点符号、运算符号等
        # 由于 t 是由英文字母组成,所以数组中有些位置的值始终不会被操作,造成了空间的浪费,比如 map[20]map[30]
        # 但这样做方便理解,比如看到 map[98] = 5 ,能知道 b 出现的频次是 5 次
        map = collections.defaultdict(int)


        # 开始统计 t 中每个字符出现的频次
        for ch in t:   #初始化需要的必要字符数量
            map[ch] += 1

        # 记录滑动窗口的长度,并且不断更新获取最小的那个
        windowLength = len(s) + 1

        # 滑动窗口的左端
        left = 0

        # 滑动窗口的右端
        right = 0

        # t 中字符的总个数
        count = len(t)

        # 滑动窗口左端新的位置
        start = 0

        # 滑动窗口的右端开始移动
        while right < len(s) :

            # 获取此时将要加入到滑动窗口的元素
            c = s[right]

            # 如果说 map 数组中 c 出现的频次大于了 0,说明此时字符 c 加入到滑动窗口距离找到这样一个子串更近了一步
            # 那么滑动窗口需要搜罗的特定元素个数变少了
            if map[c] > 0 : 
                # 需要搜罗的和 t 中字符一样的元素个数变少了
                count -= 1

            # 既然滑动窗口中新增了一个字符 c,那么 map 数组中对应的频次就需要减 1
            map[c] -= 1

        


            # 如果此时 count == 0 ,表明滑动窗口中包含了 t 中全部的字符
            # 此时,找到了一个符合条件的子串
            # 但想尝试一下,能否满足条件的情况下子串更短一些
            # 于是,去尝试把滑动窗口的左端向右移动一下
            # 可以移动的前提是,滑动窗口的左端元素抛弃后剩下的元素依旧满足条件
            # 意思就是实际上左端元素是多余的
            # 而如果这个元素对应的值在 map[] 数组中小于 0,说明它是一个多余元素
            # 反复的删除这些多余的元素
            while count == 0 : 

                # 如果当前的这个窗口值比之前维护的窗口值更小,需要进行更新
                if  right - left + 1 < windowLength : 

                    # 更新滑动窗口的长度
                    windowLength = right - left + 1

                    # 更新滑动窗口起始位置,来到了 left 这个位置
                    start = left
        

                # 接下来左端位置开始向右移动,也就是一个删除操作
                # 删除操作需要执行以下三个步骤
                # 如果这个元素不是多余的元素,比如滑动窗口为 ADBC,t 为 ABC
                # 移除了 A,那么滑动窗口又需要去新增其它的元素了
                # 所以通过 map[s.charAt(left)] == 0 来判断它是否是多余的元素
                if  map[s[left]] == 0 : 
                    # 需要搜罗的和 t 中字符一样的元素个数要增加一个了,因为删除了关键元素
                    count += 1
            
                
                # 2、这个元素离开了滑动窗口,那么在 map 中这个的值就需要加 1
                # 对应着上面新增一个元素到滑动窗口,map[c]--
                map[s[left]] += 1

                # 3、left 向右移动,那么这个元素就离开了
                left += 1

        

            # 可以开始查看新的元素了
            right += 1



        # 根据 s 中是否涵盖了 t 所有字符的子串来获取结果
        return  '' if  windowLength == (len(s) + 1) else s[start:start + windowLength]
四、复杂度分析
时间复杂度: 两个指针都严格递增,最多移动 n 次,所以总时间复杂度是 O(n)。
空间复杂度:O(C)