1. 题目解析与核心思路
LeetCode 76题"Minimum Window Substring"是字符串处理中的经典滑动窗口问题。给定字符串S和T,要求在S中找到包含T所有字符的最短子串。这个问题在2023年依然保持高频出现率,在字节跳动、亚马逊等大厂面试中多次出现。
1.1 问题重述
输入两个字符串:
- S(源字符串,长度10^5级别)
- T(目标字符模式,长度<=S)
要求输出S中最短的连续子串,使得该子串包含T中的所有字符(包括重复字符)。如果不存在则返回空字符串。
示例:
code复制S = "ADOBECODEBANC"
T = "ABC"
输出:"BANC"
1.2 暴力解法分析
最直观的解法是枚举所有可能的子串,检查是否包含T的所有字符。对于长度为n的S,子串总数是O(n^2),每个子串检查需要O(m)时间(m为T长度),总时间复杂度O(n^2*m)。当n=10^5时,这种解法显然不可行。
1.3 滑动窗口原理
滑动窗口算法通过维护一个可变大小的窗口来优化时间复杂度。核心思想是:
- 初始化左右指针left=right=0
- 右指针向右扩展,直到窗口满足条件
- 左指针向右收缩,尝试缩小窗口
- 记录满足条件的最小窗口
这种方法将时间复杂度降为O(n),因为每个字符最多被访问两次(右指针扩展和左指针收缩各一次)。
2. 详细解法实现
2.1 数据结构选择
我们需要以下数据结构:
need字典:记录T中每个字符的需求量window字典:记录当前窗口中各字符的数量valid计数器:记录当前满足需求的字符种类数
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
start, length = 0, float('inf')
2.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
2.3 边界条件处理
需要注意的特殊情况:
- T长度大于S时直接返回空串
- S或T为空字符串的情况
- T中包含S中没有的字符
- 多个最小窗口时的处理(通常返回第一个)
完整代码包含这些边界判断:
python复制 return s[start:start+length] if length != float('inf') else ""
3. 复杂度分析与优化
3.1 时间复杂度
- 初始化need字典:O(m)
- 窗口滑动过程:O(2n) → O(n)
- 总体时间复杂度:O(n + m)
3.2 空间复杂度
- need字典:O(m)
- window字典:O(m)
- 其他变量:O(1)
- 总体空间复杂度:O(m)
3.3 优化技巧
- 字符集预处理:如果已知字符集(如仅字母),可以用固定大小数组代替字典
- 提前终止:当窗口大小等于T长度时可直接返回
- 无效字符跳过:快速移动指针跳过不在T中的字符
优化后的数组实现:
python复制def minWindow(s: str, t: str) -> str:
need = [0] * 128
for c in t:
need[ord(c)] += 1
count = len(t)
left = 0
min_len = float('inf')
min_left = 0
for right in range(len(s)):
if need[ord(s[right])] > 0:
count -= 1
need[ord(s[right])] -= 1
while count == 0:
if right - left + 1 < min_len:
min_len = right - left + 1
min_left = left
need[ord(s[left])] += 1
if need[ord(s[left])] > 0:
count += 1
left += 1
return "" if min_len == float('inf') else s[min_left:min_left+min_len]
4. 常见错误与调试技巧
4.1 典型错误案例
-
valid计数错误:
- 错误:每当window[c]增加时就valid++
- 正确:只有当window[c]从need[c]-1增加到need[c]时才valid++
-
窗口更新顺序错误:
- 错误:先移动指针再更新窗口数据
- 正确:先处理当前指针位置数据再移动
-
最小窗口记录时机错误:
- 错误:在收缩循环外部记录
- 正确:在valid满足条件时立即记录
4.2 调试方法
- 打印窗口状态:
python复制print(f"left={left}, right={right}, valid={valid}, window={window}")
- 测试用例设计:
- 基础案例:"a", "a"
- 重复字符案例:"aa", "aa"
- 无解案例:"a", "b"
- 多个解案例:"cabefgecdaecf", "cae"
- 边界检查:
- 空字符串输入
- T长度大于S
- T中有重复字符
5. 同类题目拓展
掌握滑动窗口后,可以解决以下类似问题:
-
LeetCode 3. Longest Substring Without Repeating Characters
- 找不含重复字符的最长子串
- 窗口条件:所有字符计数<=1
-
LeetCode 438. Find All Anagrams in a String
- 找所有字母异位词
- 窗口大小固定为T的长度
-
LeetCode 567. Permutation in String
- 判断是否存在T的排列
- 类似438题的变种
-
LeetCode 209. Minimum Size Subarray Sum
- 找满足和≥target的最短子数组
- 窗口条件变为数字和
6. 实际应用场景
滑动窗口算法不仅在面试中常见,在实际工程中也有广泛应用:
- 网络流量控制:TCP滑动窗口协议控制数据传输
- 日志分析:在时间窗口内统计特定事件
- 实时监控:计算移动平均值或最大值
- 基因组分析:在DNA序列中寻找特定模式
7. 个人刷题建议
- 模板化练习:先掌握滑动窗口的标准模板,再处理变种
- 可视化辅助:在纸上画出窗口移动过程
- 性能对比:比较暴力解与滑动窗口解的运行时间
- 举一反三:每做完一题,思考如何修改来解决类似问题
- 错题复盘:记录错误案例,分析根本原因
提示:在面试中遇到滑动窗口问题时,建议先明确说出暴力解法的时间复杂度,再引出滑动窗口优化思路,这能展示你的分析过程。
