题目
给你一个字符串 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) 时间内解决此问题的算法吗?
思路
本题解采用滑动窗口的思路进行解答。具体步骤如下:
- 定义两个指针 left 和 right,分别指向字符串 S 的首尾位置。
- 定义两个哈希表 need 和 window,其中 need 存储字符串 T 中每个字符的数量,window 存储滑动窗口中每个字符的数量。
- 初始化 left、right 和 count 变量,其中 count 表示滑动窗口中已包含 T 中字符的数量。
- 向右移动 right 指针,如果当前字符在 need 中,则将 window 对应字符数量加 1。如果 window 中对应的字符数量等于 need 中的数量,则将 count 加 1。
- 当 count 等于字符串 T 的长度时,保存当前的子串,并将 left 指针向右移动,直到左侧的字符不在 T 中。此时仍需要将 window 对应字符数量减 1,并将 count 减 1。
重复步骤 4~5,直到 right 指针到达字符串 S 的末尾。
关键是滑动和收缩
function minWindow(s: string, t: string): string {
// 先用map记录t字串的每个字符的数量。并初始化窗口中对应的字符
const need = {};
const window = {};
for (let c of t) {
if (!need[c]) need[c] = 0;
if (!window[c]) window[c] = 0;
need[c] ++;
}
let left = 0, right = 0;// 记录窗口范围
let count = 0; // 记录有几个字符包含了
let start = 0, len = Infinity;// 记录最小目标子串的起始位置和长度
// 窗口右移
while(right < s.length) {
const c1 = s[right];
if(need[c1]) {
window[c1]++;// 窗口中找到一个有效字符
if(need[c1] === window[c1]) count ++;// 刚好该字符已经全部覆盖了
}
// 滑动
right++;
// 找到了就收缩
while(count === Object.keys(need).length) {
// 有更短
const diff = right - left;
if(diff < len) {
len = diff;
start = left;
}
// 尝试收缩窗口
const c2 = s[left];
if(need[c2]) {
window[c2]--;
if(window[c2] < need[c2]) count--// 收缩后覆盖的字符数可能受到影响
}
// 右移
left++;
}
}
return len === Infinity ? '' : s.substr(start, len);
};
- 时间复杂度:O(n)
- 空间复杂度:O(n)
总结
本题解使用滑动窗口的思路进行解答,并使用哈希表存储字符数量信息,使得时间复杂度降低至 O(n)。