最短连续子串

问题

给两个字符串:

  • 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 → 所有字符都满足了!🎉


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

第一步:初始化需求

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]=0 → less=2
    • 'D','O','B' → cnt[66]=0 → less=1
    • 'E','C' → cnt[67]=0 → less=0 ✅ 窗口 "ADOBEC" 合法!
  3. 开始收缩左边界:

    • 移除 'A' → cnt[65]=1 → less=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 大小
相关推荐
总要学点什么1 小时前
第105篇:三维特效-流动图片墙体效果——让箭头沿墙长滚,它俩竟是同一份代码
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
Agent世界2 小时前
用 CDP 做发布自动化:四道机器闸门与产物验收
前端·javascript
总要学点什么3 小时前
第104篇:三维特效-流动墙体效果——让贴图沿墙流动,它却只肯走一条对角线
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
程序员大阳3 小时前
副队长ES6+教程(3)--解构赋值
前端·javascript·es6·赋值·解构
神膘护体小月半3 小时前
css - 元素背景图由 hover 触发,初次加载时闪动的解决办法
前端·javascript·css
程序员大阳3 小时前
副队长ES6+教程(2)--let 与 const
前端·javascript·es6·const·let·var
曦樂~5 小时前
【Web Development】03JavaScript 基础和事件
开发语言·前端·javascript
码云社区5 小时前
智慧物业系统选型指南:房屋档案、账单管理与报修协作
前端·javascript·vue.js·智慧物业
code_Bo18 小时前
不只命令行:我把跨 Agent 会话同步做成了桌面应用
前端·javascript·后端
kyriewen19 小时前
AI 能把页面写出来,为什么改个需求就开始崩?
前端·javascript·人工智能