【算法-LeetCode】76. 最小覆盖子串(滑动窗口;双指针;Map)

76. 最小覆盖子串 - 力扣(LeetCode)

文章更新: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(逻辑正确,但超时)

详细的思路解释可以看官方题解。下方【我的题解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

【算法-LeetCode】76. 最小覆盖子串(滑动窗口;双指针;Map)_第1张图片

超出时间限制

【算法-LeetCode】76. 最小覆盖子串(滑动窗口;双指针;Map)_第2张图片

测试用例的长度过长时,将会超时 但是由此也可见逻辑应该是对的,就是性能不大好

成功前的尝试2(逻辑似乎错误,同样是超时)
/**
 * @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

我在浏览器里调试这个用例,发现结果非常诡异,我也搞不懂为什么会这样……

【算法-LeetCode】76. 最小覆盖子串(滑动窗口;双指针;Map)_第3张图片

我觉得逻辑是对的,但是奈何无法通过,应该是有死循环

实在没有什么好思路了,于是去看了官方题解,但是发现官方题解的思路和我上面的第一种做法是一样的,但是却没有给出具体的优化……这也太坑了吧……

我的题解1

更新: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博客

你可能感兴趣的:(LeetCode,leetcode,javascript,滑动窗口,双指针,Map)