文章更新:2021年10月12日13:47:40
给你一个字符串 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 <= 105
s 和 t 由英文字母组成
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-window-substring
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
这道题还是比较容易就能想到用滑动窗口的思路来做的,此前也做过一个滑动窗口且也是求子串的题目:
参考:【算法-LeetCode】3. 无重复字符的最长子串(滑动窗口)_赖念安的博客-CSDN博客
参考:【算法-剑指 Offer】48. 最长不含重复字符的子字符串(滑动窗口;双指针)_赖念安的博客-CSDN博客
详细的思路解释可以看官方题解。下方【我的题解1】中也对这种思路有相应解释。
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
let i = 0;
let j = 1;
let res = '';
let subStr = '';
let head = '';
for(; j <= s.length; j++) {
subStr = s.slice(i, j);
if(subStr.length < t.length) {
continue;
}
if(includeStr(subStr, t)) {
head = subStr[0];
subStr = subStr.slice(1);
while(includeStr(subStr, t)) {
head = subStr[0];
subStr = subStr.slice(1);
i++;
}
res = !res.length || res.length > subStr.length ? head + subStr : res;
}
}
return res;
// includeStr(str, target) 是用于判断字符串str中是否完整包含target子串
// 注意target中可能有重复字符
function includeStr(str, target) {
target = target.split('');
let idx = 0;
return target.every(cur => {
idx = str.indexOf(cur);
if(idx === -1) {
return false;
}
str = str.slice(0, idx) + str.slice(idx+1);
return true;
});
}
};
提交记录
执行结果:超出时间限制
最后执行的输入:"kgfidhktkjhlkbg...cghkomrbfbkoowqwgaurizliesjnve"
时间:2021/10/12 13:50
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
let res = '';
if(s.length < t.length) {
return res;
}
let lastAppear = Array.from({ length: t.length });
let temp = s;
lastAppear.map((cur, idx, arr) => {
let rear = arr.length - 1 - idx;
let last = temp.lastIndexOf(t[rear]);
temp = `${temp.slice(0, last)}0${temp.slice(last + 1)}`;
arr[rear] = last;
});
let i = new Array(t.length).fill(-1);
let subStr = '';
while (!i.every((cur, idx) => cur === lastAppear[idx])) {
let minIndex = Math.min(...i);
temp = s;
i.fill(-1);
for (let j = 0; j < i.length; j++) {
let next = temp.indexOf(t[j], minIndex + 1);
if (next === lastAppear[t.indexOf(t[j], j + 1)]) {
return res;
}
temp = `${temp.slice(0, next)}0${temp.slice(next + 1)}`;
i[j] = next;
}
subStr = s.slice(Math.min(...i), Math.max(...i) + 1);
res = !res.length || res.length > subStr.length ? subStr : res;
}
return res;
};
提交记录
执行结果:超出时间限制
最后执行的输入:"aaaaaaaaaaaabbbbbcdd","abcdd"
时间:2021/10/13 00:48
我在浏览器里调试这个用例,发现结果非常诡异,我也搞不懂为什么会这样……
实在没有什么好思路了,于是去看了官方题解,但是发现官方题解的思路和我上面的第一种做法是一样的,但是却没有给出具体的优化……这也太坑了吧……
更新:2021年10月13日13:11:36
没办法,只能在我上面的【尝试1】的基础上做一些优化了。很明显,【尝试1】中最耗费时间的就是 includeStr()
函数。所以应该从这里寻找突破口。
思考一番后还是没有很好的思路,因为 t
中如果有重复字符串的话就比较难处理。所以稍稍看了一下题解区,看到一个用字典维护 t
中字符数量的思路,感觉应该不错,所以就用这种方法来辅助判断当前子串是否包含 t
(感谢题友【@Mcdull】的分享!)。
题解参考:简简单单,非常容易理解的滑动窗口思想 - 最小覆盖子串 - 力扣(LeetCode)
详细的思路可以参看【官方题解】的相关解释,我的核心思路是一样的。有关字典维护的思路,也可以参照上面的【题解参考】,里面也有一些优化。我是用一个 Map
类型的结构来作为维护字典的。这种解法的关键仍然是【如何判断当前窗口中是否包含 t
】。
详细解释请看下方注释:
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
// 用i、j指针维护一个滑动窗口,其中i指针指向前边界,j指针指向后边界
let i = 0;
let j = 0;
// res 用于动态存储最短的覆盖子串
let res = '';
// subStr是滑动窗口变化过程中所对应的子串
let subStr = '';
// head 是一个辅助变量,用于存储subStr的第一个字符,将在i指针的移动过程中发挥作用
let head = '';
// map 是一个字典,用于维护 t 中的字符以及对应数量的映射关系
let map = new Map();
// 初始化字典
for(let ch of t) {
// 注意,对于t中的重复字符,map会累加其数量
map.set(ch, map.has(ch) ? map.get(ch)+1 : 1);
}
// 滑动窗口的后边界开始往后移动
for(; j < s.length; j++) {
// 若当前遍历字符不是t中的字符,则不必判断当前窗口中的字符是否包含t,直接遍历下一个字符
if(!map.has(s[j])) {
continue;
}
// 若当前遍历字符是t中的字符,要更新字典中对应字符的数量为原来值减1
map.set(s[j], map.get(s[j])-1);
// 接着判断当前窗口中的字符是否完整包含t,详解请看下方【补充1】
if([...map.values()].every(cur => cur <= 0)) {
// 如果完整包含t,则获取当前窗口对应的子串
subStr = s.slice(i, j+1);
// 然后开始移动i指针,也就是滑动窗口的前边界,看当前窗口中是否有更短的覆盖子串
while([...map.values()].every(cur => cur <= 0)) {
// 因为下面的slice语句会覆盖原来的subStr,所以需要在头字符丢失前将其保存
head = subStr[0];
subStr = subStr.slice(1);
// 如果i指针指向的当前字符在字典中,则需要将字典中相应字符的数量在原来的基础上加1
// 表明当前滑动窗口收缩后即将丢失一个需要的字符
if (map.has(s[i])) {
map.set(s[i], map.get(s[i]) + 1);
}
// i指针往后移动,直到当前滑动窗口中的子串已经不能完整包含t了
i++;
}
// 若当前滑动窗口对应的子串长度小于原先的覆盖子串长度,则更新res的值为最新最短的子串
// 如果当前res为空,则说明当前找到的覆盖子串为第一个符合条件的子串
res = !res.length || res.length > subStr.length ? head + subStr : res;
}
}
// 完成对s的遍历后,res中已经保存了最短的覆盖子串,将其返回
return res;
};
提交记录
266 / 266 个通过测试用例
状态:通过
执行用时:104 ms, 在所有 JavaScript 提交中击败了41.56%的用户
内存消耗:44.9 MB, 在所有 JavaScript 提交中击败了10.92%的用户
时间:2021/10/13 13:19
【补充1】
注意下这种判断逻辑在功能上其实就和上面【尝试1】中的 inclueStr()
函数一样,只不过这种方法利用了辅助的字典结构,所以会比较快。具体原理可以看上面的【参考题解】。
简单描述就是如果 map
中的字符对应数量都 <= 0
,说明当前窗口中已经有了 t
中的所有字符,此时就找到了一个覆盖子串,但是不确定当前覆盖子串是否是最短的那个,所以还要和之前获得的覆盖子串长度在长度上做比较,将 res
更新为较短的那个。当然也要移动 i
指针(即滑动窗口的前边界)。要时刻注意滑动窗口更新的条件是当前窗口中是否完整包含 t
。
如果 map
中出现了某个字符对应的数量为负数,说明,当前的滑动窗口中有重复的字符,这也是满足包含条件的。随着滑动窗口的边界移动,map
中的字符对应数量也可能会发生变化,这也是为什么可以利用 map
来判断当前窗口是否完整包含 t
。
更新:2021年7月29日18:43:21
因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。
更新:2021年10月13日24:45:21
参考:最小覆盖子串 - 最小覆盖子串 - 力扣(LeetCode)
【更新结束】
更新:2021年10月13日13:13:16
参考:简简单单,非常容易理解的滑动窗口思想 - 最小覆盖子串 - 力扣(LeetCode)
更新:2021年10月12日10:10:40
参考:Array.prototype.some() - JavaScript | MDN
参考:Array.prototype.every() - JavaScript | MDN
参考:Array.prototype.includes() - JavaScript | MDN
更新:2021年10月13日24:45:37
参考:Array.prototype.indexOf() - JavaScript | MDN
参考:Array.prototype.map() - JavaScript | MDN
参考:Array.prototype.findIndex() - JavaScript | MDN
参考:Array.prototype.find() - JavaScript | MDN
更新:2021年10月13日24:56:14
参考:【算法-LeetCode】3. 无重复字符的最长子串(滑动窗口)_赖念安的博客-CSDN博客
参考:【算法-剑指 Offer】48. 最长不含重复字符的子字符串(滑动窗口;双指针)_赖念安的博客-CSDN博客