📌 题目链接:leetcode.cn/problems/mi…
🔍 难度:困难 | 🏷️ 标签:字符串、滑动窗口、哈希表
⏱️ 目标时间复杂度:O(|s| + |t|)
💾 空间复杂度:O(|t|)
题目分析
给定字符串 s 和 t,返回 s 中涵盖 t 所有字符(含重复)的最小子串。若不存在,返回空串 ""。
关键点:
- 子串必须连续;
- 字符数量需 ≥
t中对应字符数量; - 字符区分大小写;
- 答案唯一(若有解)。
典型应用场景:文本关键词提取、日志分析、生物序列匹配等。
核心算法及代码讲解
本题采用 滑动窗口(Sliding Window) + 哈希表 解法。
🧠 算法思想
- 用两个指针
l(左)、r(右)维护窗口[l, r]。 - 扩张:移动
r,将字符加入窗口,直到窗口“可行”(包含t所需全部字符)。 - 收缩:移动
l,尝试缩小窗口,同时保持可行性,以寻找更短解。 - 记录最优:每次窗口可行时,更新最小长度与起始位置。
🗂️ 数据结构
ori:记录t中各字符的目标频次(只读)。cnt:动态记录当前窗口中各字符的实际频次。check():判断cnt是否满足ori的所有要求。
✅ 优化:
check()仅遍历ori(而非整个字符集),效率更高。
⚙️ 代码逐行注释
class Solution {
public:
unordered_map<char, int> ori, cnt;
// 检查当前窗口是否覆盖 t
bool check() {
for (const auto &p : ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c : t) {
++ori[c]; // 初始化目标频次
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1;
while (r < (int)s.size()) {
// 扩张右边界:仅关心 t 中出现的字符
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
// 收缩左边界:当窗口可行时
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l; // 记录起始位置
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]]; // 移出窗口
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
解题思路
- 初始化:统计
t的字符频次到ori。 - 滑动窗口:
r不断右移,将有效字符加入cnt;- 一旦
check()成立,进入收缩阶段; l右移,尝试缩小窗口,同时更新最优解;
- 返回结果:若找到解,截取子串;否则返回空串。
💡 关键细节:
r初始为-1,先++r再访问,避免越界;- 仅当字符在
ori中才更新cnt,忽略无关字符;ansL == -1表示无解。
算法分析
- 时间复杂度:O(C·|s| + |t|),其中 C 为字符集大小(≤52),通常视为 O(|s| + |t|)。
- 空间复杂度:O(|t|),用于存储
ori和cnt。
面试高频考点
- 滑动窗口模板:扩 → 查 → 缩 → 更新;
- 哈希表设计:只关注目标字符;
- 边界处理与空解判断;
- 可行性验证优化(可引入
valid计数器实现 O(1) 检查)。
代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
unordered_map<char, int> ori, cnt;
bool check() {
for (const auto &p: ori) {
if (cnt[p.first] < p.second) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
for (const auto &c: t) {
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, ansR = -1;
while (r < int(s.size())) {
if (ori.find(s[++r]) != ori.end()) {
++cnt[s[r]];
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
}
if (ori.find(s[l]) != ori.end()) {
--cnt[s[l]];
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
string s1 = "ADOBECODEBANC", t1 = "ABC";
cout << "Test 1: " << sol.minWindow(s1, t1) << "\n"; // "BANC"
// 测试用例 2
string s2 = "a", t2 = "a";
cout << "Test 2: " << sol.minWindow(s2, t2) << "\n"; // "a"
// 测试用例 3
string s3 = "a", t3 = "aa";
cout << "Test 3: \"" << sol.minWindow(s3, t3) << "\"\n"; // ""
return 0;
}
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第53题 —— 最大子数组和(简单)
🔹 题目:给定一个整数数组
nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
🔹 核心思路:Kadane 算法——动态规划思想,维护以当前位置结尾的最大子数组和。
🔹 考点:动态规划、贪心、前缀和优化。
🔹 难度:简单,但极其经典,是理解“局部最优推导全局最优”的典范!
💡 提示:不要用暴力 O(n²),Kadane 算法只需一次遍历 O(n) 时间、O(1) 空间!