「前端刷题」76. 最小覆盖子串

310 阅读1分钟

「这是我参与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 <= 105
  • st 由英文字母组成

 

**进阶:**你能设计一个在 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,r][l,r][l,r] 就是一个窗口,暴力穷举也是在用双指针,只是双指针移动没有外加一些约束。
  • 它将窗口扩张到不能扩张,然后让 lll 左移1,窗口再从 0 扩张到不能再扩张。
  • 如图,会出现 rrr 多次扫描同一个字符,做了很多重复的工作。

窗口的扩张

  • 扩张窗口是为了纳入目标字符,右指针右移,先找到可行解——纳入了所有目标字符。
  • 在还没找齐目标字符之前,左指针不动。因为如果此时它右移,可能丢失现有的目标字符。
  • 什么时候停止扩张窗口?——当前窗口包含了所有目标字符。
  • 此时再纳入字符,条件依然满足,但徒增子串长度。此时应该优化可行解:收窄窗口,左指针右移。

窗口的收缩

  • 保持条件满足的情况下,收缩窗口是优化可行解。当窗口不再包含所有目标字符,即有目标字符丢失,就不再收缩。
  • 此时应该扩张窗口,补充目标字符。
  • 可见,为了找到最优解,一直做两种操作之一,直到窗口的右端到达边界。

滑动窗口的套路

  1. 先找到一个可行解,再优化这个可行解。
  2. 优化到不能优化,产生出一个可能的最优解。
  3. 继续找新的可行解,再优化这个可行解。
    ……
  4. 在所有可能的最优解中,比较出最优解。

窗口的扩张或收缩的标识

  • 我们知道,窗口扩张或收缩取决于——当前窗口是否找齐所有目标字符。
  • 定义一个 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截取子串
};