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

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.dtcms.com/a/301448.html

相关文章:

  • 分布式事务:二阶段提交和三阶段提交底层原理
  • AI时代,我们更需要自己的开发方式与平台
  • java--函数式接口全面总结与使用场景指南
  • LeetCode 611.有效三角形的个数
  • python---eval函数
  • Ashampoo Background Remover(照片去背景工具) v2.0.2 免费版
  • Oracle EBS 库存期间关闭状态“已关闭未汇总”处理
  • 【成功经验分享】Github Education (Github学生认证)认证
  • 【NLP实践】一、中文短句情感二分类实现并提供RestfulApi服务调用
  • 创建属于自己的github Page主页
  • 数据结构第1问:什么是数据结构?
  • 重做日志-redo log
  • 决策树(Decision Tree)完整解析:原理 + 数学推导 + 剪枝 + 实战
  • 无向图的连通性问题
  • Qt C++ GUI 函数参数速查手册:基础与布局
  • Android 调试桥 (adb) 基础知识点
  • 通过knn算法实现识别数字
  • 【n8n教程笔记——工作流Workflow】文本课程(第一阶段)——5.4 计算预订订单数量和总金额 (Calculating booked orders)
  • nacos连接失败,启动失败常见问题
  • OpenCV-图像预处理③【图像梯度计算、边缘检测算法(如 Canny)、轮廓提取与分析、凸包特征检测,以及 轮廓的外接几何特征(如最小外接矩形、外接圆等)】
  • 硅基计划3.0 学习总结 肆 二叉树 初版
  • [每周一更]-(第148期):使用 Go 进行网页抓取:Colly 与 Goquery 的对比与思路
  • QT---概览
  • 优化Linux高并发:文件描述符与端口范围的协同调优
  • SPSC无锁环形队列技术(C++)
  • FreeRTOS—空闲任务
  • 【Python系列】Flask 应用中的主动垃圾回收
  • idea打开后project窗口未显示项目名称的解决方案
  • LangGraph快速入门项目部署
  • C++ 中实现 `Task::WhenAll` 和 `Task::WhenAny` 的两种方案