当前位置: 首页 > ops >正文

LeetCode 76:最小覆盖子串

LeetCode 76:最小覆盖子串

在这里插入图片描述

问题定义与核心挑战

给定字符串 st,需找到 s 中包含 t 所有字符(含重复)的最短子串。若不存在则返回空字符串。核心难点:

  1. 字符匹配的精确性t 中重复字符需在子串中对应数量匹配(如 t="AA",子串需至少含 2 个 A)。
  2. 高效区间搜索:直接枚举所有子串(O(n²))会超时,需通过 滑动窗口(双指针) 优化。

核心思路:滑动窗口 + 哈希表

利用 双指针(左 left、右 right 维护动态窗口,结合 哈希表 跟踪字符频率:

  1. 扩展右指针:扩大窗口,记录字符频率,直到窗口包含 t 所有字符。
  2. 收缩左指针:在窗口合法时,尝试左移缩小窗口,更新最小子串。
  3. 哈希表优化:通过 formed 变量快速判断窗口是否合法(无需每次遍历哈希表)。

算法步骤详解

步骤 1:预处理 t 的字符频率
  • 用哈希表 countT 记录 t 中每个字符的出现次数。
  • 计算 requiredt不同字符的数量(窗口需匹配这些字符的频率)。
Map<Character, Integer> countT = new HashMap<>();
for (char c : t.toCharArray()) {countT.put(c, countT.getOrDefault(c, 0) + 1);
}
int required = countT.size();
步骤 2:初始化滑动窗口变量
  • left=0:窗口左边界。
  • right=0:窗口右边界。
  • formed=0:当前窗口中满足 t 频率要求的字符数(如 t="ABC",窗口含 ABC 各至少 1 个时,formed=3)。
  • windowCounts:记录当前窗口内字符的频率。
  • minLen=∞start=0:记录最小窗口的长度和起始位置。
Map<Character, Integer> windowCounts = new HashMap<>();
int left = 0, formed = 0;
int minLen = Integer.MAX_VALUE;
int start = 0;
步骤 3:扩展右指针,构建窗口

遍历 s 的每个字符(右指针 right 移动):

  1. 更新窗口频率:将 s[right] 加入 windowCounts
  2. 判断是否满足频率要求:若 s[right]countT 中,且 windowCounts 中其频率等于 countT 中的频率,则 formed++
  3. 当窗口合法(formed == required),尝试收缩左指针
for (int right = 0; right < s.length(); right++) {char c = s.charAt(right);// 更新窗口频率windowCounts.put(c, windowCounts.getOrDefault(c, 0) + 1);// 若当前字符是t的目标字符,且频率刚满足要求,formed加1if (countT.containsKey(c) && windowCounts.get(c).intValue() == countT.get(c).intValue()) {formed++;}// 窗口合法时,收缩左指针while (formed == required) {// 更新最小窗口int currentLen = right - left + 1;if (currentLen < minLen) {minLen = currentLen;start = left;}// 收缩左指针:移除s[left]char leftChar = s.charAt(left);windowCounts.put(leftChar, windowCounts.get(leftChar) - 1);// 若移除后,该字符频率不再满足t的要求,formed减1if (countT.containsKey(leftChar) && windowCounts.get(leftChar).intValue() < countT.get(leftChar).intValue()) {formed--;}left++; // 左指针右移}
}
步骤 4:返回结果

若找到合法窗口(minLen 未被更新为 ),则截取 s[start, start+minLen);否则返回空字符串。

return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);

关键逻辑解析

  1. formed 变量的作用
    避免每次检查整个 windowCounts 是否匹配 countTO(m) 时间,mt 的不同字符数),而是通过 formed 实时跟踪已满足频率要求的字符数,达到 O(1) 判断窗口合法性。

  2. 收缩左指针的条件
    仅当窗口合法(formed == required)时,才尝试收缩,确保每次收缩都在合法区间内进行,避免遗漏更短的合法窗口。

  3. 字符频率的精确匹配
    仅当 windowCounts[c] 恰好等于 countT[c] 时,formed 才增加;收缩时,若 windowCounts[c] 小于 countT[c]formed 才减少。这保证了 formed 仅统计完全满足频率要求的字符。

完整代码(Java)

import java.util.HashMap;
import java.util.Map;class Solution {public String minWindow(String s, String t) {// 步骤1:预处理t的字符频率和requiredMap<Character, Integer> countT = new HashMap<>();for (char c : t.toCharArray()) {countT.put(c, countT.getOrDefault(c, 0) + 1);}int required = countT.size();// 滑动窗口变量初始化Map<Character, Integer> windowCounts = new HashMap<>();int left = 0, formed = 0;int minLen = Integer.MAX_VALUE;int start = 0;// 步骤2:扩展右指针,构建窗口for (int right = 0; right < s.length(); right++) {char c = s.charAt(right);// 更新窗口内字符频率windowCounts.put(c, windowCounts.getOrDefault(c, 0) + 1);// 若当前字符是t的目标字符,且频率刚满足要求,formed加1if (countT.containsKey(c) && windowCounts.get(c).intValue() == countT.get(c).intValue()) {formed++;}// 窗口合法时,收缩左指针while (formed == required) {// 更新最小窗口int currentLen = right - left + 1;if (currentLen < minLen) {minLen = currentLen;start = left;}// 收缩左指针:移除s[left]char leftChar = s.charAt(left);windowCounts.put(leftChar, windowCounts.get(leftChar) - 1);// 若移除后,该字符频率不再满足t的要求,formed减1if (countT.containsKey(leftChar) && windowCounts.get(leftChar).intValue() < countT.get(leftChar).intValue()) {formed--;}left++; // 左指针右移}}// 步骤3:返回结果return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);}
}

示例验证(以示例 1 为例)

输入s = "ADOBECODEBANC", t = "ABC"
推导过程

  1. 预处理countT = {'A':1, 'B':1, 'C':1}required=3
  2. 右指针扩展
    • right=5(字符 C),窗口 [0,5]ADOBEC):windowCountsA:1, B:1, C:1formed=3,进入收缩阶段。
    • 收缩左指针到 left=3(字符 B),窗口 [3,5]BEC):长度 3,记录为候选。
    • 继续扩展右指针,最终找到窗口 [9,11]BANC),长度 4,为最小。

复杂度分析

  • 时间复杂度O(n),其中 ns 的长度。双指针各移动 n 次,哈希表操作均为 O(1)
  • 空间复杂度O(m)mt 中不同字符的数量(最多 26 个字母,故为 O(1))。

该方法通过 滑动窗口 + 哈希表 高效解决了最小覆盖子串问题,核心在于动态维护窗口的合法性,并通过 formed 变量优化判断逻辑,确保了线性时间复杂度。

http://www.xdnf.cn/news/16424.html

相关文章:

  • mybatis的insert(pojo),会返回pojo吗
  • Petalinux生成文件的关系
  • 力扣面试150题--二进制求和
  • mmap机制
  • 2.qt调试日志输出
  • 《C++》STL--string详解(上)
  • vue3报错:this.$refs.** undefined
  • 在Podman/Docker容器中为Luckfox Lyra Zero W编译SDK:终极排错指南
  • Linux实战:从零搭建基于LNMP+NFS+DNS的WordPress博客系统
  • yolo11分类一键训练工具免安装环境windows版使用教程
  • 小白成长之路-Ansible自动化(一)
  • 20250707-2-Kubernetes 网络-Ingress暴露应用(http与https)_笔记
  • LeetCode 60:排列序列
  • 10.模块与包:站在巨人的肩膀上
  • MySQL ROUTER安装部署
  • 网络配置实验报告:主机间通信配置
  • python---eval函数
  • Day44 Java数组08 冒泡排序
  • 51核和ARM核单片机OTA实战解析(二)
  • day062-监控告警方式与Grafana优雅展示
  • 【通识】设计模式
  • Ashampoo Background Remover(照片去背景工具) v2.0.2 免费版
  • MyBatis-Plus IService 接口全量方法实现与测试(续)
  • 【Python系列】从内存分析到性能剖析
  • 【c++】从 “勉强能用” 到 “真正好用”:中文问答系统的 200 行关键优化——关于我用AI编写了一个聊天机器人……(16)
  • HBuilder X打包发布微信小程序
  • 详解力扣高频SQL50题之180. 连续出现的数字【困难】
  • Product Hunt 每日热榜 | 2025-07-27
  • 如何思考一个动态规划问题需要几个状态?
  • J2EE模式---服务层模式