大家好,我是你们的算法小伙伴。今天我们来攻克一道滑动窗口的经典困难题 ------LeetCode 76. 最小覆盖子串。这道题是滑动窗口的「标杆题目」,完美体现了「右指针扩张、左指针收缩」的核心思想,同时考察哈希表计数、匹配状态维护等综合能力,是面试中字符串处理的高频难题。
题目描述
给你两个字符串 s 和 t ,返回 s 中涵盖 t 所有字符的最小长度子串 。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。 - 进阶:设计一个在
O(m + n)时间复杂度内解决此问题的算法。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
示例 2:
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
示例 3:
输入:s = "a", t = "aa"
输出:""
解释:t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子串,返回空字符串。
提示:
m == s.lengthn == t.length1 <= m, n <= 10^5s和t由英文字母组成
解题思路
核心思路:滑动窗口 + 双哈希表
这道题的核心是动态维护一个窗口 ,保证窗口内包含 t 的所有字符(次数不低于 t 中的次数),并在所有合法窗口中找到长度最小的那个。
核心步骤:
- 统计目标字符 :用数组
need统计t中每个字符需要的次数 - 计算字符种类 :
required=t中不同字符的种类数量 - 右指针扩张:不断加入字符,更新窗口内计数
- 匹配判断 :用
match记录已满足次数要求的字符种类数 - 左指针收缩 :当
match == required时,不断收缩左指针,更新最小窗口 - 返回结果:最终截取最小窗口子串
代码实现(O (m+n) 时间复杂度)
class Solution {
public String minWindow(String s, String t) {
if (t.length() > s.length()) {
return "";
}
int[] need = new int[128];
int[] window = new int[128];
// 统计t中每个字符的出现次数
for (char c : t.toCharArray()) {
need[c]++;
}
// 计算t中不同字符的种类数
int required = 0;
for (int count : need) {
if (count > 0) {
required++;
}
}
int left = 0, right = 0;
int match = 0; // 已匹配的字符种类数
int start = 0, minLen = Integer.MAX_VALUE;
while (right < s.length()) {
// 右指针扩张
char c = s.charAt(right);
window[c]++;
if (need[c] > 0 && window[c] == need[c]) {
match++;
}
right++;
// 当所有字符种类都匹配时,收缩左指针
while (match == required) {
// 更新最小窗口
if (right - left < minLen) {
minLen = right - left;
start = left;
}
// 左指针收缩
char d = s.charAt(left);
if (need[d] > 0 && window[d] == need[d]) {
match--;
}
window[d]--;
left++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
}
代码详解
1. 数组优化说明
- 由于字符仅为英文字母(ASCII 128 个),用
int[128]数组代替HashMap,时间复杂度更优,空间复杂度为 O(1)。 need[]:t中每个字符需要出现多少次window[]:当前窗口内每个字符出现了多少次required:t 中一共有多少种不同字符(必须全部匹配才算合法)match:当前窗口内,次数已经达标的字符种类数left / right:滑动窗口双指针start / minLen:记录最小窗口的起点和长度
2. 核心变量 match
match 表示当前窗口内,次数完全满足 t 要求的字符种类数。
- 当
match等于t中不同字符的种类数时,窗口合法。 - 每次右指针加入字符时,若该字符次数刚好达标,
match++; - 每次左指针移除字符时,若该字符次数不再达标,
match--。
3. 核心逻辑
- 右指针一直往右走,把字符加入窗口
- 加入后如果某个字符次数刚好达标 →
match++ - 当
match == required→ 窗口完全合法 - 进入内层
while,拼命收缩左指针,尝试找到更小窗口 - 收缩时如果某个字符次数不达标了 →
match--,退出循环 - 最终记录最小窗口 → 截取返回
示例 1
模拟:s = "ADOBECODEBANC", t = "ABC"
need数组:A:1, B:1, C:1,其他为 0。- 右指针扩张到
right=5(字符C)时,match=3,窗口合法,开始收缩左指针,得到第一个合法窗口"ADOBEC",长度 6。 - 右指针继续扩张,直到
right=10(字符A),match=3,收缩左指针,得到最小窗口"BANC",长度 4。 - 最终返回
"BANC",与示例结果一致。
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(m+n) | 右指针遍历 s 一次(O(m)),左指针最多遍历 s 一次(O(m)),统计 t 一次(O(n)),总复杂度线性 |
| 空间复杂度 | O(1) | 数组大小固定为 128,与输入规模无关 |
高频易错点总结
- 边界处理 :
t长度大于s时,直接返回空字符串。 match统计错误 :必须保证字符次数刚好等于 目标次数时才match++,移除时次数等于 目标次数时才match--。- 窗口长度计算 :
right - left(因为right已经右移一位,无需+1)。 - 数组初始化 :
need和window数组初始化为 0,无需手动填充。 - 最终返回 :必须判断
minLen是否更新,未更新则返回空字符串。
总结
这道题是滑动窗口的经典模板题 ,核心思想是「右指针扩张找合法窗口,左指针收缩找最小窗口」,用 match 变量快速判断窗口合法性,完美实现了 O(m+n) 的线性时间复杂度,完全满足进阶要求。
- 面试优先级:这是面试中滑动窗口的必背题目,代码结构清晰,逻辑严谨,是考察字符串处理能力的标杆题。
- 拓展应用:该模板可以直接套用在「最小覆盖子串」「字符串排列」「找到所有字母异位词」等同类滑动窗口题目中。
今天的每日算法练习就到这里,我们明天再见!👋