76. 最小覆盖子串
题目
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
示例 2:
输入:s = "a", t = "a"
输出:"a"
示例 3:
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
提示:
- 1 <= s.length, t.length <= 10^5
- s 和 t 由英文字母组成
进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
滑动窗口法
这个问题其实就是枚举以每个位置为起点的最小的包含t中所有字符的s中的子串,以下一个位置为起点的最小子串可以通过滑动一个字符利用上一个窗口的字符统计信息进行,具体过程如下:
- left开始指向 0, right一直后移,直到right - left区间包含T中所有字符。记录窗口长度d
- 然后left开始逐字符滑动,如果此时窗口中仍然包含t中的所有字符,那么此时的窗口就是新的起点的最小子串,重新记录最小的窗口d。
- 然后继续滑动,直到移除的字符后窗口中不再包含T中全部的字符时停止,此时T中有一个字符没被包含在窗口中,继续后移right,直到T中的所有字符被包含在窗口,此时刚好得到以这个新的起点的窗口为包含t中所有字符的最小子串,重新记录最小的窗口d。下一个位置为起点的最小子串是在上一个位置为起点的最小子串的基础上得到的,可以利用上一个最小子串里的信息,不必从头开始获得。
- 如此循环直到S中的最后一个字符。
时间复杂度为O(n)
我们使用一个来记录窗口中的字符信息,这种针对字符数量资源的计数表示方法类似于操作系统中信号量中资源的计数表示,
- 每个字符的计数为正,说明当前滑动窗口针对这个字符还缺少的个数,
- 为0,说明当前窗口中针对这个字符数量正好够,
- 为负,说明当前滑动窗口中针对这个字符的数量是有剩余的,多余的数量为这个负数的绝对值
class Solution { public: string minWindow(string S, string T) { if(T.size() > S.size()) return ""; unordered_map<char, int> m;//映射值为窗口中还需要的这个字符的个数 for(int i = 0; i < T.size(); ++i) { if(m.find(T[i]) == m.end()) m[T[i]] = 1; else m[T[i]]++; } string ret = ""; int cnt = 0; int minLen = S.size() + 1; int left = 0,right = left; while(right < S.size()) { if(m.find(S[right]) != m.end()) { m[S[right]]--; if(m[S[right]] >= 0)//说明递减之前计数值>=1 cnt++;//触发cnt状态的改变 } while(cnt == T.size()) { if(right - left + 1 < minLen) { minLen = right - left + 1; ret = S.substr(left, minLen); } if(m.find(S[left]) != m.end()) { m[S[left]]++;//更新计数值 if(m[S[left]] > 0) --cnt;//触发cnt状态的改变 } //进行left左端的下一个遍历 ++left;//这个是公共操作 } ++right; } return ret; } };
这道题也可以不用HashMap,直接用个int的数组来代替,因为ASCII只有256个字符,所以用个大小为256的int数组即可代替HashMap,但由于一般输入字母串的字符只有128个(从128 到255为扩展ASCII 字符),所以也可以只用128,其余部分的思路完全相同。