题目描述:
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
提示:
m == s.lengthn == t.length1 <= m, n <= 105s和t由英文字母组成
实现代码
class Solution {
public:
static bool check(const unordered_map<char,int>& need_map,unordered_map<char,int>& window_map) {//检查是否覆盖了
for(auto i:need_map) {
if(i.second>window_map[i.first]) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
bool flag=0;
unordered_map<char,int> need_map;
unordered_map<char,int> window_map;
for(auto i: t) {
need_map[i]++;
}
int left=0,right=0,min=INT_MAX,minLeft=0,minRight=0;
while(right<s.size()) {//不断向右移动
window_map[s[right]]++;//更新窗口
if(check(need_map,window_map)) {//如果足够包含了
flag=1;
window_map[s[left++]]--;//尝试左指针右移
while(check(need_map,window_map)) {//直到找到不符合的那个左指针
window_map[s[left++]]--;
}
if(right-left+1<min) {//左指针会多移动一格
min=right-left+1;
minRight=right;
minLeft=left-1;
}
}
right++;
}
string ans;
if(flag==0){
return "";
}
for(int i=minLeft;i<=minRight;i++) {
ans.push_back(s[i]);
}
return ans;
}
};
1. 核心逻辑
通过滑动窗口算法实现最小覆盖子串:
- 窗口扩展:通过
right指针不断扩展窗口,直到窗口包含了t中所有字符。 - 窗口收缩:移动
left指针尝试缩小窗口,直到窗口无法再满足条件为止。 - 记录结果:每次收缩窗口后,如果当前窗口大小比之前的最优结果更小,则更新最优结果。
2. 数据结构
need_map: 一个哈希表,用于记录t中每个字符需要的次数。window_map: 一个哈希表,用于记录当前窗口中每个字符的次数。- 辅助函数
check: 比较window_map和need_map,判断当前窗口是否覆盖了t。
3. 关键变量
left和right: 滑动窗口的左右指针。min: 当前最小窗口长度。minLeft和minRight: 记录最优窗口的起点和终点。flag: 用于判断是否找到满足条件的子串。
4. 核心函数解析
check 函数
用于检查当前窗口是否满足条件:
static bool check(const unordered_map<char,int>& need_map,unordered_map<char,int>& window_map) {
for(auto i: need_map) {
if(i.second > window_map[i.first]) { // 当前窗口中某字符频次不足
return false;
}
}
return true; // 所有字符频次都满足
}
minWindow 函数
主函数,负责滑动窗口的逻辑。
步骤:
-
初始化数据结构:
- 构建
need_map,记录t中每个字符及其频次。 - 初始化滑动窗口左右指针
left和right。
- 构建
-
扩展窗口:
while(right < s.size()) { window_map[s[right]]++; // 将当前字符加入窗口 -
检查窗口有效性:
if(check(need_map, window_map)) { // 当前窗口覆盖了所有需要的字符 -
收缩窗口:
while(check(need_map, window_map)) { // 左指针右移,尽量缩小窗口 window_map[s[left++]]--; -
更新最优解:
if(right - left + 1 < min) { // 更新最小窗口 min = right - left + 1; minRight = right; minLeft = left - 1; // 左指针多移动了一格 -
最终结果: 如果
flag为0,说明未找到符合条件的子串;否则返回最优窗口。
5. 示例运行
输入:
s = "ADOBECODEBANC"
t = "ABC"
运行过程:
- 初始化:
need_map = {A:1, B:1, C:1}。 - 滑动窗口扩展:右指针逐步移动,将字符加入
window_map。 - 检查条件并收缩:在
window_map满足条件后,左指针右移,缩小窗口。 - 更新最优解:每次找到更小的窗口时更新
minLeft和minRight。 - 最终返回:
"BANC"。
6. 时间复杂度分析
-
时间复杂度: (O(n + m))
- (n) 是字符串
s的长度,滑动窗口的扩展和收缩最多执行 (n) 次。 - (m) 是字符串
t的长度,用于初始化need_map。
- (n) 是字符串
-
空间复杂度: (O(m)),
need_map和window_map需要存储t中的字符及其频次。
流程图:
flowchart TD
A[输入字符串 s 和 t] --> B[初始化 need_map 和 window_map]
B --> C[右指针向右扩展]
C --> D[更新 window_map]
D --> E{check: 是否覆盖了 t}
E -- 是 --> F[尝试移动左指针收缩窗口]
F --> G{check: 窗口仍满足条件吗?}
G -- 是 --> H[继续收缩窗口]
G -- 否 --> I[更新最小窗口结果]
E -- 否 --> C
I --> J{右指针到达末尾了吗?}
J -- 否 --> C
J -- 是 --> K{是否找到有效窗口?}
K -- 是 --> L[返回最小窗口子串]
K -- 否 --> M[返回空字符串]
关键节点说明
-
初始化阶段:
need_map保存t中每个字符的频次。window_map用于记录滑动窗口中每个字符的频次。
-
扩展右指针:
- 每次移动右指针时,将对应字符加入
window_map。
- 每次移动右指针时,将对应字符加入
-
检查覆盖条件:
- 调用
check函数,判断当前窗口是否包含t所有字符及其频次。
- 调用
-
收缩左指针:
- 如果窗口满足条件,则移动左指针尝试缩小窗口,同时更新
window_map。
- 如果窗口满足条件,则移动左指针尝试缩小窗口,同时更新
-
更新最优解:
- 如果当前窗口长度更小,则记录当前窗口的起点和长度。
-
结束判断:
- 当右指针到达末尾,判断是否找到满足条件的子串,返回结果。