1. 最小覆盖子串问题概述
LeetCode第76题"最小覆盖子串"是算法面试中的经典高频题目,也是Google、Facebook等大厂常考的Hard级别难题。题目要求我们在字符串S中找到包含字符串T所有字符的最短连续子串。这个问题完美结合了滑动窗口和哈希表两大核心算法思想,是检验面试者基础算法能力的绝佳试金石。
在实际工作中,类似的需求也经常出现。比如在文本编辑器中实现"查找并高亮显示所有关键词"功能,或者在DNA序列分析中寻找包含特定碱基组合的最短片段。理解这个问题的解法,能帮助我们处理很多字符串匹配相关的实际场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题描述与示例分析
2.1 题目具体要求
给定两个字符串S和T,要求在S中找出包含T中所有字符的最短连续子串。注意:
- T中字符的顺序不需要与子串中一致
- 如果S中不存在这样的子串,返回空字符串""
- 如果存在多个符合条件的子串,返回任意一个即可
- 时间复杂度要求通常为O(n)
示例1:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
解释:虽然"ADOBEC"包含所有字符,但"BANC"更短
2.2 边界情况考虑
在实际编码前,我们需要考虑各种边界情况:
- S比T短的情况
- T中有重复字符的情况(如T="AAB")
- S中不存在满足条件的子串
- S和T完全相等的情况
- T中包含S中没有的字符
3. 解题思路与算法选择
3.1 暴力解法分析
最直观的解法是枚举S中所有可能的子串,检查是否包含T的所有字符。这种方法的时间复杂度是O(n^3),显然无法满足题目要求。
3.2 滑动窗口算法
滑动窗口(Sliding Window)是解决这类子串/子数组问题的利器。其核心思想是维护一个窗口,通过移动窗口的左右边界来寻找最优解。对于本题,我们可以:
- 使用两个指针left和right表示窗口的左右边界
- 先移动right扩大窗口,直到包含T的所有字符
- 然后移动left缩小窗口,同时检查是否仍然满足条件
- 记录满足条件的最小窗口
3.3 哈希表辅助计数
为了高效检查窗口是否包含T的所有字符,我们需要:
- 使用哈希表need记录T中每个字符的出现次数
- 使用哈希表window记录当前窗口中各字符的出现次数
- 维护一个变量valid表示窗口中满足need条件的字符数量
4. 详细实现步骤
4.1 初始化阶段
python复制from collections import defaultdict
def minWindow(s: str, t: str) -> str:
need = defaultdict(int)
window = defaultdict(int)
for c in t:
need[c] += 1
left, right = 0, 0
valid = 0 # 满足need条件的字符数
start = 0
length = float('inf') # 初始设为无穷大
4.2 窗口滑动过程
python复制 while right < len(s):
# 移入窗口的字符
c = s[right]
right += 1
# 更新窗口数据
if c in need:
window[c] += 1
if window[c] == need[c]:
valid += 1
# 判断左侧窗口是否要收缩
while valid == len(need):
# 更新最小覆盖子串
if right - left < length:
start = left
length = right - left
# 移出窗口的字符
d = s[left]
left += 1
# 更新窗口数据
if d in need:
if window[d] == need[d]:
valid -= 1
window[d] -= 1
4.3 结果返回
python复制 return s[start:start+length] if length != float('inf') else ""
5. 复杂度分析与优化
5.1 时间复杂度
- 初始化need哈希表:O(m),m为t的长度
- 窗口滑动过程:左右指针各遍历一次s,O(n)
- 总时间复杂度:O(n + m)
5.2 空间复杂度
- 使用了两个哈希表存储字符频率
- 最坏情况下需要存储所有ASCII字符,空间复杂度O(C),C为字符集大小
5.3 优化思路
- 可以先用一个循环检查s是否包含t的所有字符,避免不必要的计算
- 对于非常大的字符集,可以使用数组代替哈希表提高访问速度
- 在移动left时,可以一次性跳过多个字符(当遇到不在t中的字符时)
6. 常见错误与调试技巧
6.1 典型错误案例
- 忘记处理t中有重复字符的情况
- 在更新valid时错误地使用了>而不是==
- 没有正确处理边界条件(如s比t短)
- 在移动left时没有正确更新window和valid
6.2 调试建议
- 先在小样例上手动模拟算法过程
- 打印出每一步的窗口状态和变量值
- 特别注意valid的更新时机
- 使用assert检查中间结果
7. 实际应用与变种问题
7.1 实际应用场景
- 文档搜索:查找包含多个关键词的最短文本片段
- 基因分析:寻找包含特定碱基序列的最短DNA片段
- 广告投放:在用户浏览记录中寻找包含多个兴趣点的最短时间段
7.2 相关变种题目
- LeetCode 3: 无重复字符的最长子串
- LeetCode 209: 长度最小的子数组
- LeetCode 438: 找到字符串中所有字母异位词
- LeetCode 567: 字符串的排列
8. 个人解题心得
在实际面试中遇到这道题时,建议按照以下步骤进行:
- 先明确问题要求,确认输入输出和边界条件
- 讨论暴力解法并分析其复杂度
- 引入滑动窗口思想,解释为什么它能优化时间复杂度
- 详细说明哈希表的使用方法和valid变量的意义
- 逐步编写代码,边写边解释
- 最后分析时间复杂度和可能的优化点
我在第一次解这道题时,因为没有正确处理valid的更新逻辑,导致窗口收缩条件判断错误。后来通过打印中间变量发现了这个问题。这也提醒我们,在实现滑动窗口算法时,要特别注意状态变量的维护。
