1. 题目解析与核心思路
Minimum Window Substring(最小覆盖子串)是LeetCode上一道经典的滑动窗口问题,编号76题。题目要求我们在字符串S中找到包含字符串T所有字符的最短连续子串。这道题在各大科技公司的面试中出现频率极高,尤其是考察候选人对滑动窗口算法的掌握程度。
我最初接触这道题时,花了整整两个小时才理清思路。后来在反复练习和总结后,发现只要抓住几个关键点,就能快速写出高效的解法。下面我会详细分享我的解题思路和优化过程。
1.1 问题重述与示例分析
给定两个字符串S和T,在S中找出包含T所有字符的最短连续子串。注意:
- 如果S中不存在这样的子串,返回空字符串""
- 如果存在多个符合条件的子串,返回任意一个即可
- T中的字符可能有重复,子串必须包含相应数量的字符
示例:
code复制输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
解释:虽然"ADOBEC"包含A、B、C,但"BANC"更短且同样包含所有字符
1.2 暴力解法与复杂度分析
最直观的解法是枚举S的所有子串,检查是否包含T的所有字符。对于长度为n的S,子串数量为O(n²),每个子串检查需要O(m)时间(m为T的长度),总时间复杂度为O(n²m)。
这种解法在LeetCode上会超时,因为当n=10⁵时,n²=10¹⁰次操作显然无法在合理时间内完成。我们需要更高效的算法。
2. 滑动窗口算法详解
滑动窗口是解决这类子串/子数组问题的利器。其核心思想是维护一个窗口,通过左右指针的移动来寻找最优解,将时间复杂度优化到O(n)。
2.1 基本滑动窗口框架
滑动窗口算法的基本框架如下:
- 初始化左右指针left=right=0,窗口为[left, right)
- 不断增加right扩大窗口,直到窗口包含所有所需字符
- 此时停止增加right,转而增加left缩小窗口
- 每次移动left时,更新结果
- 重复2-4步,直到right到达字符串末尾
对于本题,我们需要额外处理:
- 如何判断窗口包含T的所有字符?
- 如何处理T中的重复字符?
2.2 具体实现步骤
步骤1:统计字符频率
首先统计T中每个字符的出现次数:
python复制from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
步骤2:初始化窗口和计数器
python复制left = right = 0
valid = 0 # 记录窗口中满足need条件的字符个数
window = defaultdict(int) # 记录窗口中各字符的出现次数
res = ""
步骤3:滑动窗口主循环
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 not res or right - left < len(res):
res = s[left:right]
# 移出窗口的字符
d = s[left]
left += 1
# 更新窗口数据
if d in need:
if window[d] == need[d]:
valid -= 1
window[d] -= 1
return res
2.3 复杂度分析
- 时间复杂度:O(n),其中n是S的长度。左右指针各遍历一次字符串。
- 空间复杂度:O(m),其中m是T中不同字符的数量,用于存储need和window字典。
3. 关键细节与优化技巧
3.1 字符匹配的处理技巧
在实际编码中,我发现几个容易出错的地方:
- valid的更新时机:只有当window[c]等于need[c]时才增加valid,避免重复计数
- 收缩窗口的条件:valid == len(need)而非valid == len(t),因为need只存储不同字符
- 结果更新:要在收缩窗口前更新res,确保记录的是当前满足条件的最短子串
3.2 边界条件处理
特别注意以下边界情况:
- S比T短:直接返回""
- T为空字符串:根据题意通常返回""
- S中不包含T的所有字符:返回""
- 存在多个最短子串:返回任意一个即可
3.3 性能优化实践
经过多次提交测试,我发现以下优化点:
- 使用collections.defaultdict比普通dict更简洁
- 提前存储len(need)避免重复计算
- 在Python中,字符串切片s[left:right]会创建新字符串,对于极长字符串可能影响性能,可以改为记录起始位置和长度
优化后的部分代码:
python复制min_len = float('inf')
start = 0
need_cnt = len(need)
# ...在主循环中...
if right - left < min_len:
min_len = right - left
start = left
return s[start:start+min_len] if min_len != float('inf') else ""
4. 常见错误与调试技巧
4.1 典型错误案例
- 忽略重复字符:
python复制# 错误写法:仅检查字符是否存在,忽略数量
if all(c in window for c in need):
# 这会导致"aab"匹配"aabb"时出错
- 指针移动顺序错误:
python复制# 错误写法:先移动left再检查
while valid == need_cnt:
left += 1 # 这样会漏掉left=0的情况
# 正确应该先检查再移动
- 结果更新不及时:
python复制# 错误写法:在收缩循环外更新res
while valid == need_cnt:
left += 1
res = s[left:right] # 这样记录的是收缩后的窗口
4.2 调试方法
当你的代码不能通过所有测试用例时,可以:
- 打印窗口变化过程:
python复制print(f"left={left}, right={right}, window={window}, valid={valid}")
- 使用简单测试用例:
code复制S="a", T="a"
S="aa", T="aa"
S="ab", T="b"
- 检查边界条件处理是否完备
5. 扩展与变种问题
掌握了这道题后,可以尝试解决以下变种问题:
5.1 LeetCode 567. Permutation in String
判断s2是否包含s1的排列,解法类似但判断条件变为valid == len(need)且窗口大小等于len(s1)。
5.2 LeetCode 438. Find All Anagrams in a String
找到s中所有p的字母异位词的起始索引,需要收集所有满足条件的窗口。
5.3 LeetCode 3. Longest Substring Without Repeating Characters
最长无重复字符子串,窗口收缩条件变为出现重复字符。
5.4 带容差的变种
如果允许最多k个字符不匹配,如何修改算法?这时需要维护一个不匹配计数器,在valid >= len(need)-k时考虑更新结果。
6. 滑动窗口算法模板总结
经过多次练习,我总结出滑动窗口的通用模板:
python复制def slidingWindow(s: str, t: str) -> str:
from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
need_cnt = len(need)
window = defaultdict(int)
left = right = valid = 0
res = ""
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 == need_cnt:
# 更新结果
if not res or right - left < len(res):
res = s[left:right]
d = s[left]
left += 1
# 更新左指针逻辑
if d in need:
if window[d] == need[d]:
valid -= 1
window[d] -= 1
return res
使用时只需根据具体问题调整:
- 右指针移动时的更新逻辑
- 窗口收缩条件
- 结果更新条件
- 左指针移动时的更新逻辑
7. 实战经验分享
在面试中遇到这道题时,建议按以下步骤进行:
- 先明确问题要求,确认边界条件
- 提出暴力解法并分析复杂度
- 引入滑动窗口优化思路
- 逐步实现算法,边写边解释
- 讨论时间/空间复杂度
- 考虑可能的优化和变种
我曾在面试中被要求现场实现这个算法,当时犯了一个错误:在收缩窗口时先移动指针再检查条件,导致漏掉了第一个字符。面试官指出后,我立即意识到问题并修正,最终获得了积极评价。这个经历让我明白:
- 写代码时要格外注意指针移动的顺序
- 即使犯了错,能快速定位并修正也是加分项
- 提前准备常见问题的模板非常有用
最后给刷题的朋友一个建议:这道题至少要亲手实现3遍,间隔几天重复练习,直到能15分钟内无bug写出。滑动窗口是面试中的常客,掌握它能解决一大类字符串处理问题。
