一、题目描述
给你一个字符串 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 由英文字母组成
三、解题思路
- 基本思路:
滑动窗口【难点在于缩小窗口的时机和缩小到什么时候为止】 - 具体思路:
- 定义:ans 表示答案,初始化为空字符串;f 表示字符串 t 中每个字母和其频率的映射关系;c 表示 f 中频率大于 0 的数量,初始化为 f 的大小 。
- 滑动窗口:
- 加入字符:每次循环加入一个字符到滑动窗口中,判断它是否存在于 f 的映射中,存在则其频率 -1 ;如果频率为 0 表示该字符已经用尽,则 c - 1 。【频率可以为负数,负数表示倒欠,后面舍弃字符时可以进行多次舍弃】
- 舍弃字符:一旦 c==0 ,表示字符串 t 中所有字符都用过了,这时候可以开始舍弃字符;从头部开始舍弃字符,每舍弃一个字符,判断其是否存在 f 的映射中,存在则频率 +1 ;如果频率为 1 ,表示字符串 t 中有 1 个字符没有被利用,这时候该滑动窗口内的字符串就是一个局部最小满足题目条件的字符串,记录到 ans 中,然后 c++ ;
- 返回最小的满足条件的 ans 。
四、参考代码
时间复杂度:
O
(
n
)
\Omicron(n)
O(n) 【指针 i 和 j 只会往前走,最坏情况也就是走两遍字符数 s ,所以复杂度是
O
(
n
)
\Omicron(n)
O(n) 】
空间复杂度:
O
(
1
)
\Omicron(1)
O(1) 【map 的 key 是 char,最大也就 128 个,可以看作常量空间】
class Solution {
public:
map<char, int> init(string t, int n) {
map<char, int> ans;
for (int i = 0; i < n; i++) {
ans[t[i]]++;
}
return ans;
}
string minWindow(string s, string t) {
int n = s.length(), m = t.length();
string ans = "";
map<char, int> f = init(t, m);
int c = f.size();
for (int i = 0, j = 0; j < n; j++) {
if (f.count(s[j]) == 1) { // 加入
f[s[j]]--;
if (f[s[j]] == 0)
c--;
}
while (c == 0) { // 舍弃
if (f.count(s[i]) == 1) {
f[s[i]]++;
if (f[s[i]] == 1) {
ans = (ans.length() > j - i + 1 || ans == "")
? s.substr(i, j - i + 1)
: ans;
c++;
}
}
i++;
}
}
return ans;
}
};