一、问题概述
给定两个字符串 s和 t,在 s 中找出包含 t 所有字符的最小长度子串,若不存在则返回空字符串。
核心需求:① 子串必须包含 t 的全部字符(含重复字符);② 子串长度尽可能小;③ 时间复杂度需高效。
示例:输入 s="ADOBECODEBANC",t="ABC",输出 "BANC"。
二、核心解法:滑动窗口
1. 解法思想
滑动窗口是一种基于双指针的高效策略,通过维护一个"动态窗口"在主串中移动,实现"扩张-收缩"的循环,从而在一次遍历中找到满足条件的最小窗口。核心逻辑分为两步:
-
扩张右边界 :用右指针遍历主串,将字符纳入窗口,直到窗口包含
t的所有字符(满足"有效性"); -
收缩左边界:当窗口有效时,移动左指针缩小窗口范围,直到窗口不再满足条件,此过程中记录最小窗口的位置和长度。
本质:通过窗口的动态调整,避免暴力解法中"枚举所有子串"的冗余计算,将时间复杂度从 O(n²) 优化至 O(n)(n 为主串长度)。
2. 关键数据结构与变量
| 变量/结构 | 作用 | 示例(t="ABC") |
|---|---|---|
哈希表 need |
统计 t 中各字符的"需求数量"(即需要包含的次数) |
need = {'A':1, 'B':1, 'C':1} |
左指针 left |
窗口左边界,控制窗口收缩 | 初始为 0,随条件动态右移 |
右指针 right |
窗口右边界,控制窗口扩张 | 从 0 遍历至 s 末尾 |
计数器 valid |
记录窗口中"已满足需求"的字符种类数(当 valid = need.size() 时窗口有效) | 窗口包含 A、B 时,valid=2;包含 A、B、C 时,valid=3(有效) |
min_len |
记录最小窗口的长度,初始为无穷大 | 找到有效窗口后更新,最终用于判断是否存在结果 |
start |
记录最小窗口的起始索引,用于最终截取子串 | 窗口缩至最小时更新 |
3. 完整执行流程
(以 s="ADOBECODEBANC"、t="ABC" 为例)
-
初始化:need = {'A':1, 'B':1, 'C':1},left=0,valid=0,min_len=INT_MAX,start=0;
-
扩张右边界(right=0 至 4):窗口为 "ADOBE",包含 A、B,valid=2(未满足);
-
扩张至 right=5:窗口纳入 'C',need['C'] 变为 0,valid=3(满足条件);此时窗口为 "ADOBEC",长度 6,更新 min_len=6,start=0;
-
收缩左边界:左移 left 至 1(移出 'A'),need['A'] 变为 1,valid=2(不再满足);停止收缩,此时最小窗口仍为 "ADOBEC";
-
继续扩张与收缩:right 继续移动至 10(纳入 'B'、'A' 等),当窗口为 "CODEBA" 时再次满足条件,收缩左边界至 8,窗口变为 "BANC",长度 4,更新 min_len=4,start=8;
-
遍历结束:截取 s[8:12],得到结果 "BANC"。
cpp
class Solution {
public:
string minWindow(string s, string t) {
// 特殊情况:t为空或s长度小于t,直接返回空
if (t.empty() || s.length() < t.length()) return "";
// 1. 初始化需求哈希表
unordered_map<char, int> need;
for (char c : t) need[c]++;
// 2. 滑动窗口核心变量
int left = 0, right = 0;
int valid = 0;
int min_len = INT_MAX;
int start = 0;
// 3. 扩张右边界
while (right < s.length()) {
char c = s[right];
right++; // 右指针右移,纳入当前字符
// 若当前字符是t需要的,更新需求统计
if (need.count(c)) {
need[c]--;
// 该字符的需求刚好满足(从1→0),有效种类数+1
if (need[c] == 0) valid++;
}
// 4. 窗口有效时,收缩左边界
while (valid == need.size()) {
// 更新最小窗口信息
if (right - left < min_len) {
min_len = right - left;
start = left;
}
// 左指针右移,移出当前字符
char d = s[left];
left++;
// 若移出的是t需要的字符,更新需求
if (need.count(d)) {
// 该字符的需求从满足→不满足(从0→1),有效种类数-1
if (need[d] == 0) valid--;
need[d]++;
}
}
}
// 5. 结果判断:若min_len未更新,说明无有效窗口
return min_len == INT_MAX ? "" : s.substr(start, min_len);
}
};
四、注意事项
1. 哈希表的"需求计数"逻辑
need 表中字符的计数可能为负数,代表该字符在窗口中"超额出现"。例如 t="A",窗口中包含两个 'A',则 need['A'] = -1。仅当计数从 1 变为 0 时,valid 才加 1;仅当计数从 0 变为 1 时,valid 才减 1------避免因超额字符误判窗口有效性。
2. 特殊边界处理
-
t 为空字符串:按题目要求返回空(或根据场景定义);
-
s 长度小于 t:直接返回空,不可能包含 t 的所有字符;
-
无有效窗口:min_len 始终为 INT_MAX,最终返回空。
3. 窗口收缩的终止条件
收缩左边界的循环条件是 valid == need.size()(窗口有效),一旦 valid 减小(窗口不再满足)则停止收缩,避免过度收缩导致遗漏后续更小窗口。
4. 最小窗口的记录时机
必须在收缩左边界的过程中记录最小窗口,因为此时窗口刚满足条件,收缩过程中才可能找到更小的有效窗口;若在扩张时记录,窗口未经过收缩,长度并非最优。
五、总结
通过双指针控制窗口的扩张与收缩,用哈希表和计数器判断窗口有效性,在一次遍历中完成最优解查找。