题意
给你一个字符串 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.length
n == t.length
1 <= m, n <= 105
s
和t
由英文字母组成
进阶:你能设计一个在 o(m+n)
时间内解决此问题的算法吗?
思路
核心思路:滑动窗口
关键:
- right++扩大窗口找可行解 ——> left++缩小窗口找更优解 ——> 判断该更优解是否可能是最优解并对应更新变量 ——> left++继续往后找更多可行解
- 变量的更新时机:
-
关于left和right的更新:
- left更新:缩小窗口找更优解时,以及,找到更优解后继续开始找下一个可行解时
- right更新:扩大窗口时
-
关于need数组和needCnt的更新:
- 窗口进一个字符就need[字符]–,如果该字符是窗口需要的字符就再needCnt–;
- 窗口出一个字符就need[字符]++,如果该字符是窗口需要的字符就再needCnt++
-
关于start和minlen的更新:
- 找到一个更优解时,若窗口大小<minlen,就说明该更优解可能成为最优解,因此更新minlen和start
-
问题:
- 怎样判断窗口已经包含了t中所有字符?
- 缩小窗口找更优解怎么实现?
解答:
-
如果我们用两个vector来判断:
- 两个vector分别存放窗口中字符频次和t中字符频次。
- 那么每次判断窗口是否包含t中字符就需要遍历vector判断对应t中字符的出现频次与窗口中对应字符的出现频次是否相等。
如果只用一个vector来判断:
- 这个vector存放········的差值,即窗口中每个t中字符还需要出现多少次。
- 那么每次仍需要遍历这个vector判断对应t中字符的出现频次是否为0。
如果只用一个变量needCnt来判断:
-
这个变量表示窗口中还需要出现的t中字符的总个数
-
那么每次只需要根据needCnt是否为0,就能直接判断窗口是否包含了t中的所有字符。
❓ ❓❓ 为什么能直接根据needCnt判断窗口是否已经包含了t中的所有字符?
- 只有在窗口进来需要的字符(need[字符]>0)时才会让needCnt–,如果need[字符]<=0就不会让needCnt–,因此不会误判。每次让needCnt–的字符都一定是窗口还需要的t中字符。
-
如果need[s[left]] < 0就一直缩小窗口,直到遇到必须包含的字母。因为:
窗口每进一个字符就会need[字符]–,need[s[left]]<0就说明s[left]是窗口本不需要包含的字符(包括本就不属于t、属于t但所需个数多余了的字符),就可以直接跳过。
步骤:
- right不断右移直到窗口包含所有t中的字母,找到一个可行解
- left++不断右移缩小窗口直到遇到必须包含的字母,找到一个最优解
- 找到一个更优解之后,判断该更优解是否可能是最优解。若窗口大小<minlen,就说明该更优解可能成为最优解,更新与结果相关的变量
- left右移找下一个可行解。重复执行上述步骤直至s末尾。
代码
无注释版
string minWindow(string s, string t) {
vector<int> need(128);
for (auto& c : t) need[c]++;
int left = 0, right = 0;
int start = 0, minlen = INT_MAX;
int needCnt = t.size();
while (right < s.size()) {
if (need[s[right++]]-- > 0) {
if (--needCnt == 0) {
while (need[s[left]] < 0) need[s[left++]]++;
if (right - left < minlen) {
minlen = right - left;
start = left;
}
need[s[left++]]++;
needCnt++;
}
}
}
return minlen == INT_MAX ? "" : s.substr(start, minlen);
}
带注释版
string minWindow(string s, string t) {
vector<int> need(128);
for (auto& c : t) need[c]++;//记录t中字符频次
int left = 0, right = 0;
int start = 0, minlen = INT_MAX;//表示满足条件的最小子串的首位和长度,便于最后substr截取子串
int needCnt = t.size();//窗口需要包含的t中字符数
while (right < s.size()) {
//right++扩大窗口
//如果s[right]不是窗口需要的t中字符,则直接做完if(need[s[right++]]-- > 0)判断就走下一循环继续right++扩大窗口了
if (need[s[right++]]-- > 0) { //说明窗口需要s[right]
if (--needCnt == 0) { //说明窗口包含了所有t中字符,找到了一个可行解
//接下来找更优解
while (need[s[left]] < 0) need[s[left++]]++;//left++缩小窗口直到遇到必须包含的字母
if (right - left < minlen) { //若窗口大小<minlen,就说明该更优解可能成为最优解,更新与结果相关的变量
minlen = right - left;
start = left;
}
//left++使当前 更优解窗口 不再满足条件,继续找下一个可行解。
//同时因为left++前s[left]一定是窗口需要包含的字母,因此needCnt也要记得++
need[s[left++]]++;
needCnt++;
}
}
}
return minlen == INT_MAX ? "" : s.substr(start, minlen);
}
复杂度分析
-
时间复杂度:O(n)
-
空间复杂度:O(1)