「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」。
题目
链接:leetcode-cn.com/problems/mi…
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例 1:
**输入:**s = "ADOBECODEBANC", t = "ABC" 输出:"BANC"
示例 2:
**输入:**s = "a", t = "a" 输出:"a"
示例 3:
输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
提示:
1 <= s.length, t.length <= 105s和t由英文字母组成
**进阶:**你能设计一个在 o(n) 时间内解决此问题的算法吗?
解题思路
- 以
S = 'aaat',T = 't'为例: - [l:r][ l:r][l:r] 表示一个子串。
- l=0l=0l=0 时,rrr 有 4 个选择; l=1l=1l=1 时, rrr 有 3 个选择; l=2l=2l=2 时, rrr 有 2 个选择 ……
- l=0l=0l=0 时,rrr 有 4 个选择; l=1l=1l=1 时, rrr 有 3 个选择; l=2l=2l=2 时, rrr 有 2 个选择 ……
- [l,r][l,r][l,r] 就是一个窗口,暴力穷举也是在用双指针,只是双指针移动没有外加一些约束。
- 它将窗口扩张到不能扩张,然后让 lll 左移1,窗口再从 0 扩张到不能再扩张。
- 如图,会出现 rrr 多次扫描同一个字符,做了很多重复的工作。
窗口的扩张
- 扩张窗口是为了纳入目标字符,右指针右移,先找到可行解——纳入了所有目标字符。
- 在还没找齐目标字符之前,左指针不动。因为如果此时它右移,可能丢失现有的目标字符。
- 什么时候停止扩张窗口?——当前窗口包含了所有目标字符。
- 此时再纳入字符,条件依然满足,但徒增子串长度。此时应该优化可行解:收窄窗口,左指针右移。
窗口的收缩
- 保持条件满足的情况下,收缩窗口是优化可行解。当窗口不再包含所有目标字符,即有目标字符丢失,就不再收缩。
- 此时应该扩张窗口,补充目标字符。
- 可见,为了找到最优解,一直做两种操作之一,直到窗口的右端到达边界。
滑动窗口的套路
- 先找到一个可行解,再优化这个可行解。
- 优化到不能优化,产生出一个可能的最优解。
- 继续找新的可行解,再优化这个可行解。
…… - 在所有可能的最优解中,比较出最优解。
窗口的扩张或收缩的标识
- 我们知道,窗口扩张或收缩取决于——当前窗口是否找齐所有目标字符。
- 定义一个 missingType 变量,表示当前缺失的字符种类数(还要找齐几种字符)。
- 它的初始值,为 input 字符串的字符种类数。
- 当它为 0 时,表示没有缺少任何种类,找齐了所有目标字符。
missingType 取决于 各个字符的缺失情况
- 用一个哈希表,去存各个目标字符和对应的缺失个数。
- 比如输入字符串为 'baac',则 map 初始为
{ a: 2, b: 1, c: 1 },这些值是动态的,比如窗口新纳入一个 a,则 map["a"] 减 1。map["a"] 为 0 代表不缺 a 了,a 字符找齐了。
代码
const minWindow = (s, t) => {
let minLen = s.length + 1;
let start = s.length; // 结果子串的起始位置
let map = {}; // 存储目标字符和对应的缺失个数
let missingType = 0; // 当前缺失的字符种类数
for (const c of t) { // t为baac的话,map为{a:2,b:1,c:1}
if (!map[c]) {
missingType++; // 需要找齐的种类数 +1
map[c] = 1;
} else {
map[c]++;
}
}
let l = 0, r = 0; // 左右指针
for (; r < s.length; r++) { // 主旋律扩张窗口,超出s串就结束
let rightChar = s[r]; // 获取right指向的新字符
if (map[rightChar] !== undefined) map[rightChar]--; // 是目标字符,它的缺失个数-1
if (map[rightChar] == 0) missingType--; // 它的缺失个数新变为0,缺失的种类数就-1
while (missingType == 0) { // 当前窗口包含所有字符的前提下,尽量收缩窗口
if (r - l + 1 < minLen) { // 窗口宽度如果比minLen小,就更新minLen
minLen = r - l + 1;
start = l; // 更新最小窗口的起点
}
let leftChar = s[l]; // 左指针要右移,左指针指向的字符要被丢弃
if (map[leftChar] !== undefined) map[leftChar]++; // 被舍弃的是目标字符,缺失个数+1
if (map[leftChar] > 0) missingType++; // 如果缺失个数新变为>0,缺失的种类+1
l++; // 左指针要右移 收缩窗口
}
}
if (start == s.length) return "";
return s.substring(start, start + minLen); // 根据起点和minLen截取子串
};