🧩 题目
给你一个字符串 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.lengthn == t.length1 <= m, n <= 105s和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);
};
🔑 核心思想:滑动窗口 + 双指针
这是一个典型的 双指针 + 滑动窗口 的问题。基本思路如下:
- 使用两个指针
left和right表示当前窗口的左右边界。 - 先不断向右移动
right,扩大窗口,直到窗口包含t中的所有字符。 - 然后尝试向右移动
left,缩小窗口,在保证窗口仍然包含所有目标字符的前提下,尽可能让窗口更小。 - 不断重复上述过程,记录满足条件的最小窗口。
🔍 分步详解
✅ 第一步:初始化目标字符计数数组
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的长度。minStart和minEnd: 记录最小覆盖子串的起始和结束下标。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中所有的字符(每种字符的数量都 >= 要求),我们尝试缩小窗口以找到更小的覆盖子串。 - 如果当前窗口比之前记录的更短,就更新
minStart和minEnd。 - 然后将左指针向右移动,移出窗口的字符要恢复其在
charCountInWindow中的数量。 - 如果移出的是恰好满足
t要求的字符(即移出前它的数量是 0),则requiredCharTypesNotMet++,因为现在又缺了这个字符。
✅ 第六步:返回结果
return minStart === -1 ? "" : s.substring(minStart, minEnd + 1);
- 如果
minStart仍然是-1,说明没找到符合条件的子串,返回空字符串。 - 否则,根据
minStart和minEnd截取对应的子串并返回。
🧮 示例解析
举个例子来帮助理解:
s = "ADOBECODEBANC"
t = "ABC"
初始化 charCountInWindow:
- A → 1
- B → 1
- C → 1
requiredCharTypesNotMet = 3
然后滑动窗口不断扩展、收缩,直到找到最小的覆盖子串 "BANC" 或 "BANC" 等。
🧠 时间复杂度分析
- 时间复杂度: O(N),其中 N 是字符串
s的长度。每个字符最多被左右指针访问两次。 - 空间复杂度: O(1),固定大小的数组(128 个 ASCII 字符)。