LeetCode 热题 100 之第12题 最小覆盖子串(JavaScript篇)

95 阅读4分钟

传送门:76. 最小覆盖子串 - 力扣(LeetCode)

🧩 题目

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。

✅ 示例

示例 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 由英文字母组成

🛠️解题代码

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function(s, t) {
    const charCountInWindow = Array(128).fill(0);
    let requiredCharTypesNotMet = 0;

    for (let c of t) {
        const code = c.codePointAt(0);
        if (charCountInWindow[code] === 0) {
            requiredCharTypesNotMet++;
        }
        charCountInWindow[code]++;
    }

    const n = s.length;
    let minStart = -1;
    let minEnd = n;
    let left = 0;

    for (let right = 0; right < n; right++) {
        const rightCharCode = s[right].codePointAt(0);
        charCountInWindow[rightCharCode]--;
        if (charCountInWindow[rightCharCode] === 0) {
            requiredCharTypesNotMet--;
        }

        while (requiredCharTypesNotMet === 0) {
            if (right - left < minEnd - minStart) {
                minStart = left;
                minEnd = right;
            }

            const leftCharCode = s[left].codePointAt(0);
            if (charCountInWindow[leftCharCode] === 0) {
                requiredCharTypesNotMet++;
            }
            charCountInWindow[leftCharCode]++;
            left++;
        }
    }

    return minStart === -1 ? "" : s.substring(minStart, minEnd + 1);
};

🔑 核心思想:滑动窗口 + 双指针

这是一个典型的 双指针 + 滑动窗口 的问题。基本思路如下:

  1. 使用两个指针 left 和 right 表示当前窗口的左右边界。
  2. 先不断向右移动 right,扩大窗口,直到窗口包含 t 中的所有字符。
  3. 然后尝试向右移动 left,缩小窗口,在保证窗口仍然包含所有目标字符的前提下,尽可能让窗口更小。
  4. 不断重复上述过程,记录满足条件的最小窗口。

🔍 分步详解

✅ 第一步:初始化目标字符计数数组

const charCountInWindow = Array(128).fill(0);
let requiredCharTypesNotMet = 0;
  • charCountInWindow: 这是一个大小为 128 的数组,用于记录每个字符在 t 中需要出现的次数。
  • requiredCharTypesNotMet: 表示当前窗口中还有多少种字符没有满足 t 的要求(即数量不足)。

✅ 第二步:统计目标字符的需求

for (let c of t) {
    const code = c.codePointAt(0);
    if (charCountInWindow[code] === 0) {
        requiredCharTypesNotMet++;
    }
    charCountInWindow[code]++;
}
  • 遍历 t 中的每个字符,将其 ASCII 编码作为索引,在 charCountInWindow 中记录该字符的出现次数。
  • 如果某个字符是第一次出现,则 requiredCharTypesNotMet++,表示我们需要关注这种字符是否在窗口中满足了需求。

比如 t = 'abc',那么 charCountInWindow['a'.charCodeAt(0)] = 1 等等。


✅ 第三步:滑动窗口主循环

const n = s.length;
let minStart = -1;
let minEnd = n;
let left = 0;
  • n: 字符串 s 的长度。
  • minStartminEnd: 记录最小覆盖子串的起始和结束下标。
  • left: 滑动窗口左指针。

✅ 第四步:移动右指针扩展窗口

for (let right = 0; right < n; right++) {
    const rightCharCode = s[right].codePointAt(0);
    charCountInWindow[rightCharCode]--;
    if (charCountInWindow[rightCharCode] === 0) {
        requiredCharTypesNotMet--;
    }
  • right: 右指针,从左到右扫描整个字符串 s
  • 将当前字符加入窗口,即将其在 charCountInWindow 中的数量减一。
  • 如果这个字符的数量刚好变成 0,说明它在窗口中的出现次数达到了 t 中的要求,因此 requiredCharTypesNotMet--

✅ 第五步:尝试收缩窗口(当窗口已满足条件时)

while (requiredCharTypesNotMet === 0) {
    // 当前窗口包含所有目标字符,尝试更新最小窗口
    if (right - left < minEnd - minStart) {
        minStart = left;
        minEnd = right;
    }

    const leftCharCode = s[left].codePointAt(0);
    if (charCountInWindow[leftCharCode] === 0) {
        requiredCharTypesNotMet++;
    }
    charCountInWindow[leftCharCode]++;
    left++;
}
  • 此时窗口内已经包含了 t 中所有的字符(每种字符的数量都 >= 要求),我们尝试缩小窗口以找到更小的覆盖子串。
  • 如果当前窗口比之前记录的更短,就更新 minStartminEnd
  • 然后将左指针向右移动,移出窗口的字符要恢复其在 charCountInWindow 中的数量。
  • 如果移出的是恰好满足 t 要求的字符(即移出前它的数量是 0),则 requiredCharTypesNotMet++,因为现在又缺了这个字符。

✅ 第六步:返回结果

return minStart === -1 ? "" : s.substring(minStart, minEnd + 1);
  • 如果 minStart 仍然是 -1,说明没找到符合条件的子串,返回空字符串。
  • 否则,根据 minStartminEnd 截取对应的子串并返回。

🧮 示例解析

举个例子来帮助理解:

s = "ADOBECODEBANC"
t = "ABC"

初始化 charCountInWindow

  • A → 1
  • B → 1
  • C → 1
  • requiredCharTypesNotMet = 3

然后滑动窗口不断扩展、收缩,直到找到最小的覆盖子串 "BANC""BANC" 等。


🧠 时间复杂度分析

  • 时间复杂度: O(N),其中 N 是字符串 s 的长度。每个字符最多被左右指针访问两次。
  • 空间复杂度: O(1),固定大小的数组(128 个 ASCII 字符)。