力扣打卡每日一题————最小覆盖子串

一、问题概述

给定两个字符串 st,在 s 中找出包含 t 所有字符的最小长度子串,若不存在则返回空字符串。

核心需求:① 子串必须包含 t 的全部字符(含重复字符);② 子串长度尽可能小;③ 时间复杂度需高效。

示例:输入 s="ADOBECODEBANC"t="ABC",输出 "BANC"

二、核心解法:滑动窗口

1. 解法思想

滑动窗口是一种基于双指针的高效策略,通过维护一个"动态窗口"在主串中移动,实现"扩张-收缩"的循环,从而在一次遍历中找到满足条件的最小窗口。核心逻辑分为两步:

  • 扩张右边界 :用右指针遍历主串,将字符纳入窗口,直到窗口包含 t 的所有字符(满足"有效性");

  • 收缩左边界:当窗口有效时,移动左指针缩小窗口范围,直到窗口不再满足条件,此过程中记录最小窗口的位置和长度。

本质:通过窗口的动态调整,避免暴力解法中"枚举所有子串"的冗余计算,将时间复杂度从 O(n²) 优化至 O(n)(n 为主串长度)。

2. 关键数据结构与变量

变量/结构 作用 示例(t="ABC")
哈希表 need 统计 t 中各字符的"需求数量"(即需要包含的次数) need = {'A':1, 'B':1, 'C':1}
左指针 left 窗口左边界,控制窗口收缩 初始为 0,随条件动态右移
右指针 right 窗口右边界,控制窗口扩张 从 0 遍历至 s 末尾
计数器 valid 记录窗口中"已满足需求"的字符种类数(当 valid = need.size() 时窗口有效) 窗口包含 A、B 时,valid=2;包含 A、B、C 时,valid=3(有效)
min_len 记录最小窗口的长度,初始为无穷大 找到有效窗口后更新,最终用于判断是否存在结果
start 记录最小窗口的起始索引,用于最终截取子串 窗口缩至最小时更新

3. 完整执行流程

(以 s="ADOBECODEBANC"、t="ABC" 为例)

  1. 初始化:need = {'A':1, 'B':1, 'C':1},left=0,valid=0,min_len=INT_MAX,start=0;

  2. 扩张右边界(right=0 至 4):窗口为 "ADOBE",包含 A、B,valid=2(未满足);

  3. 扩张至 right=5:窗口纳入 'C',need['C'] 变为 0,valid=3(满足条件);此时窗口为 "ADOBEC",长度 6,更新 min_len=6,start=0;

  4. 收缩左边界:左移 left 至 1(移出 'A'),need['A'] 变为 1,valid=2(不再满足);停止收缩,此时最小窗口仍为 "ADOBEC";

  5. 继续扩张与收缩:right 继续移动至 10(纳入 'B'、'A' 等),当窗口为 "CODEBA" 时再次满足条件,收缩左边界至 8,窗口变为 "BANC",长度 4,更新 min_len=4,start=8;

  6. 遍历结束:截取 s[8:12],得到结果 "BANC"。

cpp 复制代码
class Solution {
public:
    string minWindow(string s, string t) {
        // 特殊情况:t为空或s长度小于t,直接返回空
        if (t.empty() || s.length() < t.length()) return "";

        // 1. 初始化需求哈希表
        unordered_map<char, int> need;
        for (char c : t) need[c]++;
        
        // 2. 滑动窗口核心变量
        int left = 0, right = 0;
        int valid = 0;
        int min_len = INT_MAX;
        int start = 0;

        // 3. 扩张右边界
        while (right < s.length()) {
            char c = s[right];
            right++; // 右指针右移,纳入当前字符

            // 若当前字符是t需要的,更新需求统计
            if (need.count(c)) {
                need[c]--;
                // 该字符的需求刚好满足(从1→0),有效种类数+1
                if (need[c] == 0) valid++;
            }

            // 4. 窗口有效时,收缩左边界
            while (valid == need.size()) {
                // 更新最小窗口信息
                if (right - left < min_len) {
                    min_len = right - left;
                    start = left;
                }

                // 左指针右移,移出当前字符
                char d = s[left];
                left++;

                // 若移出的是t需要的字符,更新需求
                if (need.count(d)) {
                    // 该字符的需求从满足→不满足(从0→1),有效种类数-1
                    if (need[d] == 0) valid--;
                    need[d]++;
                }
            }
        }

        // 5. 结果判断:若min_len未更新,说明无有效窗口
        return min_len == INT_MAX ? "" : s.substr(start, min_len);
    }
};

四、注意事项

1. 哈希表的"需求计数"逻辑

need 表中字符的计数可能为负数,代表该字符在窗口中"超额出现"。例如 t="A",窗口中包含两个 'A',则 need['A'] = -1。仅当计数从 1 变为 0 时,valid 才加 1;仅当计数从 0 变为 1 时,valid 才减 1------避免因超额字符误判窗口有效性。

2. 特殊边界处理

  • t 为空字符串:按题目要求返回空(或根据场景定义);

  • s 长度小于 t:直接返回空,不可能包含 t 的所有字符;

  • 无有效窗口:min_len 始终为 INT_MAX,最终返回空。

3. 窗口收缩的终止条件

收缩左边界的循环条件是 valid == need.size()(窗口有效),一旦 valid 减小(窗口不再满足)则停止收缩,避免过度收缩导致遗漏后续更小窗口。

4. 最小窗口的记录时机

必须在收缩左边界的过程中记录最小窗口,因为此时窗口刚满足条件,收缩过程中才可能找到更小的有效窗口;若在扩张时记录,窗口未经过收缩,长度并非最优。

五、总结

通过双指针控制窗口的扩张与收缩,用哈希表和计数器判断窗口有效性,在一次遍历中完成最优解查找。

相关推荐
ada7_7 小时前
LeetCode(python)230.二叉搜索树中第k小的元素
python·算法·leetcode·链表
TL滕8 小时前
从0开始学算法——第十五天(滑动窗口练习)
笔记·学习·算法
DuHz8 小时前
milliLoc 论文精读:把商用毫米波 FMCW 的绝对测距从“厘米栅格”推进到“毫米级连续值”,并顺带修正 AoA 的系统相位偏差
论文阅读·物联网·算法·信息与通信·毫米波雷达
qq_401700418 小时前
Linux文件锁解决多进程并发
linux·服务器·算法
长安er9 小时前
LeetCode 83/237/82 链表删除问题-盒子模型
数据结构·算法·leetcode·链表·力扣
小虎牙0079 小时前
RSA 的核心原理
算法
重生之后端学习9 小时前
56. 合并区间
java·数据结构·后端·算法·leetcode·职场和发展
小猪猪屁9 小时前
顺序表与链表:头插法与尾插法详解
c语言·数据结构·c++
历程里程碑9 小时前
C++ 5:模板初阶
c语言·开发语言·数据结构·c++·算法