leetcode:76. 最小覆盖子串
题目
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
注意:
- 对于
t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 - 如果
s中存在这样的子串,我们保证它是唯一的答案。
示例:
输入:s = "ADOBECODEBANC", t = "ABC"输出:"BANC"
输入:s = "a", t = "a"输出:"a"
输入: s = "a", t = "aa"输出: ""解释: t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。
解答 & 代码
滑动窗口:
- 定义左闭右开的滑动窗口
[left, right),初始化窗口长度为 0,即left = 0,right = 0 - 寻找一个可行解:先不断增大
right指针,使得窗口右边界扩大,直到窗口内包含的字符满足条件 - 优化可行解,找到最优解:然后,不断增加
left指针,使得窗口左边界收缩,直到窗口内包含的字符不再不再满足条件 - 循环重复第 2、3 步,直到
right走到字符串s的尽头
另外,如何判断窗口内的字符是否满足条件?
- 需要设置
**need**和**window**两个哈希表,作为计数器,分别记录字符串 t 中出现的字符及出现的次数、窗口中的字符及出现的次数(只记录need包含的字符)。 - 设置一个
**validChCnt**,代表窗口中满足need条件的字符个数。每次窗口扩张,window增加某个字符的计数,如果更新后该字符计数恰好等于need中该字符的计数,则更新后该字符变得满足条件,validChCnt+1;每次窗口收缩,window减少某个字符的计数,如果更新前该字符计数恰好等于need中该字符的计数,则更新后该字符不再满足条件,validChCnt-1 - 如果
validChCnt == need.size(),则窗口内的字符满足条件
滑动窗口题目,可以直接套模板,只需要考虑 4 个问题:
- 当
right指针右移,窗口右边界扩张,即加入字符时,应该更新哪些数据?
- 应当增加
window对应字符的计数,如果增加计数后窗口中该字符个数 =need中该字符需要的个数,则validChCnt+1
- 什么条件下,应该暂停扩大窗口,开始
left指针右移、收缩窗口左边界?
- 当满足 need 条件的字符个数
validChCnt==need需要的字符种类数,窗口左侧开始收缩,优化可行解,寻找最优解
- 当
left指针右移,窗口左边界收缩,即移出字符时,应该更新哪些数据?
- 应当减少
window对应字符的计数,如果减少计数前窗口中该字符个数 =need中该字符需要的个数,则validChCnt-1
- 题目要求的结果应该在扩大窗口时更新,还是在收缩窗口时更新?
- 应该在收缩窗口时更新最终结果,因为求的是最优解
另外,注意:C++ 的 **unordered_map**,通过 **map[key]** 访问对应的值,如果这个 **key** 不存在,会自动创建这个 **key**,并将 **map[key]** 赋值为 0,因此无需先判断哈希表中是否存在 key,可以直接使用 ++map[key]
class Solution {public:string minWindow(string s, string t) {// 设置两个哈希表,相当于计数器unordered_map<char, int> need; // 记录字符串 t 中出现的字符及出现的次数unordered_map<char, int> window; // 记录窗口中的字符及出现的次数,只记录 need 包含的字符// 初始化,将 t 中出现的字符和出现的次数存入哈希表 needfor(int i = 0; i < t.size(); ++i)++need[t[i]];int minStart = 0; // 最小覆盖子串的起点int minLen = INT_MAX; // 最小覆盖子串的长度// 1. 初始化窗口长度为 0,即左、右指针设为 0,窗口是左闭右开区间 [left, right)int left = 0; // 左指针(含)int right = 0; // 右指针(不含)int validChCnt = 0; // 代表窗口中满足 need 条件的字符个数// 滑动窗口while(right < s.size()){char c = s[right]; // 将移入窗口的字符// 2. right 指针右移,窗口扩大++right;// 窗口内数据的更新:若当前字符是 need 需要的,窗口内该字符计数 +1// 若更新后窗口内该字符计数 == need 需要该字符的数量,则满足条件的字符个数 +1if(need.find(c) != need.end()){++window[c];if(window[c] == need[c])++validChCnt;}// debug// cout << "window: [" << left << ", " << right << "]" << endl;// 判断窗口左侧是否要收缩,即 left 指针是否要右移// 当满足 need 条件的字符个数 validChCnt == need 的字符种类数,窗口左侧收缩while(validChCnt == need.size()){// 更新最小覆盖字串if(right - left < minLen){minStart = left;minLen = right - left;}// 以下步骤和窗口右侧扩大是对称的char deleteC = s[left]; // 将要删除的字符// 3. left 指针右移,窗口收缩++left;// 窗口内数据的更新:若当前字符是 need 需要的,窗口内该字符计数 -1// 若更新后窗口内该字符计数 < need 需要该字符的数量,则满足条件的字符个数 -1if(need.find(deleteC) != need.end()){if(window[deleteC] == need[deleteC])--validChCnt;--window[deleteC];}// debug// cout << "window: [" << left << ", " << right << "]" << endl;}}return minLen == INT_MAX ? "" : s.substr(minStart, minLen);}};
复杂度分析:设字符串 s 长为 N,字符串t 包含 C 个不同的字符
- 时间复杂度 O(N):最坏情况下,字符串
s的每个字符分别被左、右指针遍历一次 - 空间复杂度 O(C):两个哈希表的空间复杂度都取决于字符串
t中不同字符的种数 C
执行结果:
执行结果:通过执行用时:12 ms, 在所有 C++ 提交中击败了 80.22% 的用户内存消耗:7.7 MB, 在所有 C++ 提交中击败了 42.72% 的用户
