问题
给两个字符串:
s:主串(比如"ADOBECODEBANC")t:目标串(比如"ABC")
找出 s 中最短的连续子串,使得这个子串 包含 t 中所有字符(包括重复次数) 。
比如
t = "AAB",那子串里至少要有 2 个'A'和 1 个'B'。
如果找不到,返回空字符串 ""。
核心思想:滑动窗口(双指针)
想象一个可伸缩的窗口在 s 上滑动:
右指针(right) :不断向右扩展,把字符“吃进来”
左指针(left) :当窗口已经“满足条件”时,尝试向右收缩,看看能不能变得更短
我们的目标是:在所有“满足条件”的窗口中,找到长度最小的那个。
cnt 数组 + less 变量
1. cnt 数组(大小 128)
用来记录 每个字符还需要多少个。
初始时,根据 t 设置需求:
比如 t = "ABC" → cnt[65] = 1('A'),cnt[66] = 1('B'),cnt[67] = 1('C')
当我们在 s 中遇到某个字符,就 减 1(表示“我提供了一个”)
如果 cnt[c] 从 1 变成 0 → 这个字符的需求刚好满足
如果变成负数(比如 -1)→ 这个字符多出来了(冗余)
2. less 变量
表示 还有多少种字符没有满足需求。
初始值 = t 中不同字符的种类数(不是总长度!)
t = "AAB" → 有 'A' 和 'B' 两种 → less = 2
每当某个字符的需求从 >0 变成 0,less--
当 less === 0 → 所有字符都满足了!🎉
🚶 算法步骤详解(配合你的代码)
第一步:初始化需求
for (let c of t) {
const code = c.codePointAt(0);
if (cnt[code] === 0) less++; // 第一次见到这个字符
cnt[code]++; // 记录需要多少个
}
✅ 此时 cnt 存的是“还需要多少”,less 是“还差几种”。
第二步:滑动窗口遍历 s
右指针扩展(吃进新字符)
const c = s[right].codePointAt(0);
cnt[c]--; // 提供了一个 c,所以需求减少
if (cnt[c] === 0) less--; // 如果刚好满足,种类数减一
左指针收缩(当窗口合法时)
while (less === 0) { // 当前窗口已覆盖 t
// 更新答案:如果当前窗口更短,就记录
if (right - left < ansRight - ansLeft) {
ansLeft = left;
ansRight = right;
}
// 尝试移除左边字符
const x = s[left].codePointAt(0);
if (cnt[x] === 0) less++; // 移除后,x 不够了!
cnt[x]++; // 需求增加(因为少了一个)
left++;
}
重点:只有当
cnt[x] === 0时,移除它才会导致“不满足”。
因为如果cnt[x] < 0(冗余),移除一个没关系!
第三步:返回结果
return ansLeft < 0 ? "" : s.substring(ansLeft, ansRight + 1);
- 如果从未找到合法窗口(
ansLeft还是 -1),返回"" - 否则返回
[ansLeft, ansRight]的子串
🌰 举个完整例子
s = "ADOBECODEBANC", t = "ABC"
-
初始化
cnt:
cnt[65]=1 ('A'), cnt[66]=1 ('B'), cnt[67]=1 ('C'), 其他为 0
less = 3 -
右指针移动:
'A'→cnt[65]=0→less=2'D','O','B'→cnt[66]=0→less=1'E','C'→cnt[67]=0→less=0✅ 窗口"ADOBEC"合法!
-
开始收缩左边界:
- 移除
'A'→cnt[65]=1→less=1❌ 停止收缩 - 当前最短长度 = 6
- 移除
-
继续右移……直到找到
"BANC"(长度 4),更新答案。
最终返回 "BANC"。
/**
* @param {string} s
* @param {string} t
* @return {string}
*/
var minWindow = function(s, t) {
// 频次数组,ASCII 共 128 个字符
const cnt = Array(128).fill(0);
let less = 0; // 表示还有多少种字符未满足需求
// 初始化 t 中每个字符的需求
for (let c of t) {
const code = c.codePointAt(0);
if (cnt[code] === 0) {
less++;
}
cnt[code]++;
}
const m = s.length;
let ansLeft = -1;
let ansRight = m; // 初始设为一个较大值(长度 m 不可能)
let left = 0;
for (let right = 0; right < m; right++) {
const c = s[right].codePointAt(0);
cnt[c]--;
// 如果某个字符刚好被“补足”(从正变0),说明这种字符达标了
if (cnt[c] === 0) {
less--;
}
// 当所有字符都满足(less === 0),尝试收缩左边界
while (less === 0) {
// 更新最小窗口
if (right - left < ansRight - ansLeft) {
ansLeft = left;
ansRight = right;
}
const x = s[left].codePointAt(0);
// 如果移除 s[left] 会导致某种字符不足
if (cnt[x] === 0) {
less++; // 缺失一种字符
}
cnt[x]++;
left++;
}
}
return ansLeft < 0 ? "" : s.substring(ansLeft, ansRight + 1);
};
为什么这个方法高效?
- 时间复杂度:O(n) —— 每个字符最多被访问两次(右指针一次,左指针一次)
- 空间复杂度:O(1) ——
cnt固定 128 大小