主题
前置知识:滑动窗口
如果您不知道滑动窗口,推荐先看视频 滑动窗口【基础算法精讲 03】,并完成 209. 长度最小的子数组 作为本题的铺垫,因为这两题都属于「越长越合法」滑动窗口。
什么是「涵盖」
看示例 1,
滑动窗口怎么滑
原理和 209 题一样,按照视频中的做法,我们枚举
具体来说:
- 初始化
,用来记录最短子串的左右端点,其中 是 的长度。 - 用一个哈希表(或者数组)
统计 中每个字母的出现次数。 - 初始化
,以及一个空哈希表(或者数组) ,用来统计 子串中每个字母的出现次数。 - 遍历
,设当前枚举的子串右端点为 ,把 的出现次数加一。 - 遍历
中的每个字母及其出现次数,如果出现次数都大于等于 中的字母出现次数: - 如果
,说明我们找到了更短的子串,更新 。 - 把
的出现次数减一。 - 左端点右移,即
加一。 - 重复上述三步,直到
有字母的出现次数小于 中该字母的出现次数为止。
- 如果
- 最后,如果
,说明没有找到符合要求的子串,返回空字符串,否则返回下标 到下标 之间的子串。
由于本题大写字母和小写字母都有,为了方便,代码实现时可以直接创建大小为
优化前
python
# 请选择 Python3 提交代码,而不是 Python
class Solution:
def minWindow(self, s: str, t: str) -> str:
cnt_s = Counter() # s 子串字母的出现次数
cnt_t = Counter(t) # t 中字母的出现次数
ans_left, ans_right = -1, len(s)
left = 0
for right, c in enumerate(s): # 移动子串右端点
cnt_s[c] += 1 # 右端点字母移入子串
while cnt_s >= cnt_t: # 涵盖
if right - left < ans_right - ans_left: # 找到更短的子串
ans_left, ans_right = left, right # 记录此时的左右端点
cnt_s[s[left]] -= 1 # 左端点字母移出子串
left += 1
return "" if ans_left < 0 else s[ans_left: ans_right + 1]cpp
// C++ 版待补充cpp
class Solution {
bool is_covered(int cnt_s[], int cnt_t[]) {
for (int i = 'A'; i <= 'Z'; i++) {
if (cnt_s[i] < cnt_t[i]) {
return false;
}
}
for (int i = 'a'; i <= 'z'; i++) {
if (cnt_s[i] < cnt_t[i]) {
return false;
}
}
return true;
}
public:
string minWindow(string s, string t) {
int cnt_s[128]{}; // s 子串字母的出现次数
int cnt_t[128]{}; // t 中字母的出现次数
for (char c : t) {
cnt_t[c]++;
}
int m = s.size();
int ans_left = -1, ans_right = m;
int left = 0;
for (int right = 0; right < m; right++) { // 移动子串右端点
cnt_s[s[right]]++; // 右端点字母移入子串
while (is_covered(cnt_s, cnt_t)) { // 涵盖
if (right - left < ans_right - ans_left) { // 找到更短的子串
ans_left = left; // 记录此时的左右端点
ans_right = right;
}
cnt_s[s[left]]--; // 左端点字母移出子串
left++;
}
}
return ans_left < 0 ? "" : s.substr(ans_left, ans_right - ans_left + 1);
}
};复杂度分析
- 时间复杂度:
,其中 是 的长度, 是 的长度, 是字符集合的大小,本题字符均为英文字母,所以 。注意 只会增加不会减少, 每增加一次,我们就花费 的时间。因为 至多增加 次,所以二重循环的时间复杂度为 ,再算上统计 字母出现次数的时间 ,总的时间复杂度为 。 - 空间复杂度:
。如果创建了大小为 的数组,则 。
优化
上面的代码每次都要花费
可以。用一个变量
设
如何维护
为了方便实现,把
- 如果字母
进入窗口后, ,这意味着 在子串和 中的出现次数从 变成了 ,那么把 增加一。 - 如果字母
离开窗口前, ,这意味着 离开窗口后, 在子串和 中的出现次数从 变成了 ,那么把 减少一。
⚠注意:不能在
python
class Solution:
def minWindow(self, s: str, t: str) -> str:
# 注:defaultdict 比 Counter 快
diff = defaultdict(int) # 窗口每种字母个数 - t 每种字母个数
for c in t:
diff[c] -= 1
kinds = len(diff) # t 中有 kinds 种不同的字母
ans_left, ans_right = -1, len(s)
ge_cnt = 0 # 窗口内有 ge_cnt 种字母的出现次数 >= t 中相应字母的出现次数
left = 0
for right, c in enumerate(s): # 移动子串右端点
diff[c] += 1 # 右端点字母移入子串
if diff[c] == 0: # 原来窗口内 c 的出现次数比 t 的少,现在一样多
ge_cnt += 1 # 从 < 变成 >=
while ge_cnt == kinds: # 涵盖:所有字母的出现次数都是 >=
if right - left < ans_right - ans_left: # 找到更短的子串
ans_left, ans_right = left, right # 记录此时的左右端点
x = s[left] # 左端点字母
if diff[x] == 0:
# x 移出窗口之前,检查出现次数,
# 如果窗口内 x 的出现次数和 t 一样,
# 那么 x 移出窗口后,窗口内 x 的出现次数比 t 的少
ge_cnt -= 1 # 从 >= 变成 <
diff[x] -= 1 # 左端点字母移出子串
left += 1
return "" if ans_left < 0 else s[ans_left: ans_right + 1]cpp
// C++ 版待补充cpp
class Solution {
public:
string minWindow(string s, string t) {
int diff[128]{}; // 窗口每种字母个数 - t 每种字母个数
int kinds = 0;
for (char c : t) {
if (diff[c] == 0) {
kinds++; // 统计 t 有多少个不同的字母
}
diff[c]--;
}
int m = s.size();
int ans_left = -1, ans_right = m;
int ge_cnt = 0; // 窗口内有 ge_cnt 种字母的出现次数 >= t 中相应字母的出现次数
int left = 0;
for (int right = 0; right < m; right++) { // 移动子串右端点
char c = s[right]; // 右端点字母
diff[c]++; // 右端点字母移入子串
if (diff[c] == 0) { // 原来窗口内 c 的出现次数比 t 的少,现在一样多
ge_cnt++; // 从 < 变成 >=
}
while (ge_cnt == kinds) { // 涵盖:所有字母的出现次数都是 >=
if (right - left < ans_right - ans_left) { // 找到更短的子串
ans_left = left; // 记录此时的左右端点
ans_right = right;
}
char x = s[left]; // 左端点字母
if (diff[x] == 0) {
// x 移出窗口之前,检查出现次数,
// 如果窗口内 x 的出现次数和 t 一样,
// 那么 x 移出窗口后,窗口内 x 的出现次数比 t 的少
ge_cnt--; // 从 >= 变成 <
}
diff[x]--; // 左端点字母移出子串
left++;
}
}
return ans_left < 0 ? "" : s.substr(ans_left, ans_right - ans_left + 1);
}
};复杂度分析
- 时间复杂度:
或 ,其中 是 的长度, 是 的长度, 。注意 只会增加不会减少,二重循环的时间复杂度为 。使用哈希表写法的时间复杂度为 ,数组写法的时间复杂度为 。 - 空间复杂度:
。无论 和 有多大,额外空间都不会超过 。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府