最短连续子串

问题

给两个字符串:

  • 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 变成 0less--

less === 0 → 所有字符都满足了!🎉


🚶 算法步骤详解(配合你的代码)

第一步:初始化需求

js 复制代码
for (let c of t) {
    const code = c.codePointAt(0);
    if (cnt[code] === 0) less++; // 第一次见到这个字符
    cnt[code]++;                 // 记录需要多少个
}

✅ 此时 cnt 存的是"还需要多少",less 是"还差几种"。


第二步:滑动窗口遍历 s

右指针扩展(吃进新字符)

js 复制代码
const c = s[right].codePointAt(0);
cnt[c]--; // 提供了一个 c,所以需求减少
if (cnt[c] === 0) less--; // 如果刚好满足,种类数减一

左指针收缩(当窗口合法时)

js 复制代码
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(冗余),移除一个没关系!


第三步:返回结果

js 复制代码
return ansLeft < 0 ? "" : s.substring(ansLeft, ansRight + 1);
  • 如果从未找到合法窗口(ansLeft 还是 -1),返回 ""
  • 否则返回 [ansLeft, ansRight] 的子串

🌰 举个完整例子

s = "ADOBECODEBANC", t = "ABC"

  1. 初始化 cnt:
    cnt[65]=1 ('A'), cnt[66]=1 ('B'), cnt[67]=1 ('C'), 其他为 0
    less = 3

  2. 右指针移动:

    • 'A'cnt[65]=0less=2
    • 'D','O','B'cnt[66]=0less=1
    • 'E','C'cnt[67]=0less=0 ✅ 窗口 "ADOBEC" 合法!
  3. 开始收缩左边界:

    • 移除 'A'cnt[65]=1less=1 ❌ 停止收缩
    • 当前最短长度 = 6
  4. 继续右移......直到找到 "BANC"(长度 4),更新答案。

最终返回 "BANC"


js 复制代码
/**
 * @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 大小
相关推荐
萧曵 丶15 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
Amumu1213816 小时前
Vue3扩展(二)
前端·javascript·vue.js
NEXT0616 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
牛奶18 小时前
你不知道的 JS(上):原型与行为委托
前端·javascript·编译原理
牛奶18 小时前
你不知道的JS(上):this指向与对象基础
前端·javascript·编译原理
牛奶18 小时前
你不知道的JS(上):作用域与闭包
前端·javascript·电子书
pas13619 小时前
45-mini-vue 实现代码生成三种联合类型
前端·javascript·vue.js
颜酱20 小时前
数组双指针部分指南 (快慢·左右·倒序)
javascript·后端·算法
兆子龙20 小时前
我成了🤡, 因为不想看广告,花了40美元自己写了个鸡肋挂机脚本
android·javascript
SuperEugene20 小时前
枚举不理解?一文让你醍醐灌顶
前端·javascript