华为 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)