目录
[1 题目概述](#1 题目概述)
[1.1 题目要求](#1.1 题目要求)
[1.2 核心考点](#1.2 核心考点)
[1.3 解题核心](#1.3 解题核心)
[2 核心解题思路](#2 核心解题思路)
[3 代码逐行详解](#3 代码逐行详解)
[3.1 字符频次数组初始化](#3.1 字符频次数组初始化)
[3.2 右指针扩窗逻辑](#3.2 右指针扩窗逻辑)
[3.3 左指针缩窗逻辑](#3.3 左指针缩窗逻辑)
[3.4 结果返回](#3.4 结果返回)
[4 复杂度分析](#4 复杂度分析)
[5 解题关键要点总结](#5 解题关键要点总结)
[6 小结](#6 小结)
最小覆盖子串是 LeetCode 上的经典 Hard 题,也是滑动窗口(双指针) 算法的典型应用场景。这道题的核心难点在于如何高效找到包含目标串所有字符的最短连续子串,而滑动窗口能通过一次遍历实现线性时间复杂度,是该题的最优解法。本文将聚焦核心思路与代码解析,摒弃冗余内容,让你快速掌握这道题的解题关键。
1 题目概述
1.1 题目要求
给定两个字符串 s(母串)和 t(目标串),在 s 中找到包含 t 所有字符(含重复次数) 的最短连续子串,若不存在则返回空字符串。
题目链接:76. 最小覆盖子串 - 力扣(LeetCode)
1.2 核心考点
- 滑动窗口(双指针)的动态维护(扩大 / 收缩);
- 字符频次的高效统计(数组替代哈希表);
- 窗口有效性的快速判断。
1.3 解题核心
用右指针扩大窗口找到包含t所有字符的有效窗口,再用左指针收缩窗口寻找最短有效子串,配合字符数组统计频次,实现高效求解。
2 核心解题思路
采用滑动窗口 + 固定长度字符数组的组合方案,核心逻辑分 3 步:
- 频次统计 :用两个长度为 128 的数组(覆盖所有 ASCII 字符),分别统计
t的字符需求(need数组)和当前窗口的字符频次(window数组),比哈希表更高效;- 双指针维护窗口 :左、右指针构成左闭右开 的滑动窗口,
right指针向右扩窗,left指针仅在窗口有效时向左缩窗;- 有效性判断 :用
needCount记录t的不同字符种类数,valid记录窗口中满足「频次达标」的字符种类数,当valid == needCount时,窗口包含t所有字符,即为有效窗口。
3 代码逐行详解
本题的 Java 实现代码简洁且高效,核心逻辑集中在滑动窗口的维护,以下分模块解析核心代码(完整代码附注释):
java
class Solution {
public String minWindow(String s, String t) {
// 1. 初始化字符频次数组(覆盖所有ASCII字符,效率高于哈希表)
int[] window = new int[128]; // 统计当前窗口字符频次
int[] need = new int[128]; // 统计t的字符需求频次
int needCount = 0; // t中不同字符的种类数
// 遍历t,填充need数组并统计needCount
for (char c : t.toCharArray()) {
if (need[c] == 0) needCount++; // 新字符种类,计数+1
need[c]++;
}
// 2. 初始化滑动窗口相关变量
int left = 0, right = 0; // 窗口左右边界(左闭右开)
int valid = 0; // 窗口中频次达标的字符种类数
int start = 0; // 最短有效子串的起始索引
int len = Integer.MAX_VALUE; // 最短有效子串长度(初始为极大值)
// 3. 右指针向右扩大窗口
while (right < s.length()) {
char c = s.charAt(right);
right++; // 先移指针,再处理(左闭右开,窗口范围[left, right))
// 仅处理t需要的字符
if (need[c] > 0) {
window[c]++;
// 该字符频次达标,valid+1
if (window[c] == need[c]) valid++;
}
// 4. 窗口有效时,左指针收缩寻找最短子串
while (valid == needCount) {
// 更新最短子串信息
if (right - left < len) {
start = left;
len = right - left;
}
// 收缩左边界
char d = s.charAt(left);
left++;
// 仅处理t需要的字符
if (need[d] > 0) {
// 该字符频次即将不达标,valid-1
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
// 5. 返回结果:未找到则返回空,否则返回最短子串
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
3.1 字符频次数组初始化
- 用长度**
128的数组** 替代**HashMap** ,直接通过字符 ASCII 码作为索引,存取效率更高,且空间固定;- **
needCount**的作用:避免每次判断窗口有效性时遍历整个数组,仅通过「种类数匹配」即可快速判断,提升效率。
3.2 右指针扩窗逻辑
- 窗口定义为左闭右开
[left, right),简化边界计算(窗口长度直接为right - left);- 仅当字符是
t需要的(need[c] > 0)时,才更新窗口频次数组,避免无意义计算;- 当字符频次刚好达标 (
window[c] == need[c])时,valid才 + 1,确保valid统计的是「完全满足需求」的字符种类。
3.3 左指针缩窗逻辑
- 仅在窗口有效(
valid == needCount)时缩窗,核心目标是寻找更短的有效子串;- 缩窗前先更新最短子串的
start和len,保证记录的是当前最优解;- 收缩时若移除的是
t需要的字符,且该字符频次刚好达标 ,则valid-1,窗口变为无效,退出缩窗循环。
3.4 结果返回
- 若
len仍为初始的Integer.MAX_VALUE,说明未找到有效子串,返回空;- 否则通过
substring(start, start + len)截取最短子串(左闭右开,与窗口定义一致)。
4 复杂度分析
- 时间复杂度 :O(m+n),其中
m是s的长度,n是t的长度。s中每个字符最多被left和right指针各遍历一次,无嵌套循环;- 空间复杂度:O(1),因两个统计数组长度固定为 128,与输入字符串的长度无关,属于常数空间。
5 解题关键要点总结
① 滑动窗口的核心 :右指针负责**「找有效解」** ,左指针负责**「优化有效解」**,一次遍历即可找到最优解,避免暴力解法的O(m*m*n)高复杂度;
② 数组替代哈希表 :处理 ASCII 字符时,用长度 128 的数组作为频次统计容器,存取效率远高于HashMap;
③ valid 与 needCount 的设计:通过「字符种类数匹配」判断窗口有效性,避免每次遍历频次数组,将判断效率从O(128)降为O(1);
④ 左闭右开的窗口定义 :简化窗口长度计算和边界处理,无需额外处理+1/-1的问题;
⑤ 仅处理目标字符 :扩窗和缩窗时,只对t需要的字符做频次更新,减少无意义的代码执行。
6 小结
最小覆盖子串的解题关键在于滑动窗口的动态维护 和高效的频次统计与有效性判断。
核心记住:扩窗找有效,缩窗求最优,配合简单的统计容器,就能高效解决这类问题。
感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!
