优质博文:IT-BLOG-CN
一、题目
给你一个字符串s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果s
中不存在涵盖t
所有字符的子串,则返回空字符串"" 。
对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。
如果s
中存在这样的子串,我们保证它是唯一的答案。
示例 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.length
n == t.length
1 <= m, n <= 105
s
和t
由英文字母组成
进阶: 你能设计一个在o(m+n)
时间内解决此问题的算法吗?
二、代码
思想: 我们用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的r
指针,和一个用于「收缩」窗口的l
指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在s
上滑动窗口,通过移动r
指针不断扩张窗口。当窗口包含t
全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
如何判断当前的窗口包含所有t
所需的字符呢?我们可以用一个哈希表表示t
中所有的字符以及它们的个数,用一个哈希表动态维护窗口中所有的字符以及它们的个数,如果这个动态表中包含t
的哈希表中的所有字符,并且对应的个数都不小于t
的哈希表中各个字符的个数,那么当前的窗口是「可行」的。
注意:这里
t
中可能出现重复的字符,所以我们要记录字符的个数。
优化: 如果s=XX⋯XABCXXXX,t=ABC
,那么显然[XX⋯XABC]
是第一个得到的「可行」区间,得到这个可行区间后,我们按照「收缩」窗口的原则更新左边界,得到最小区间。我们其实做了一些无用的操作,就是更新右边界的时候「延伸」进了很多无用的X
,更新左边界的时候「收缩」扔掉了这些无用的X
,做了这么多无用的操作,只是为了得到短短的ABC
。没错,其实在s
中,有的字符我们是不关心的,我们只关心t
中出现的字符,我们可不可以先预处理s
,扔掉那些t
中没有出现的字符,然后再做滑动窗口呢?也许你会说,这样可能出现XXABXXC
的情况,在统计长度的时候可以扔掉前两个X
,但是不扔掉中间的X
,怎样解决这个问题呢?优化后的时空复杂度又是多少?这里代码给出没有优化的版本。
class Solution {// 1、通过 hashMap 记录 t 中的字符串和个数// 2、通过 fast slow 快慢指针记录最短字符串的位置// 3、通过 hashMap 记录当前符合要求的字符串和个数Map<Character, Integer> ori = new HashMap();// 定义一个变量,保存字串的大小,并将符合要求的字串fast/slow指针赋值给resL,resRint fast = 0, slow = 0, len = Integer.MAX_VALUE, resL = -1, resR = -1;Map<Character, Integer> cur = new HashMap();public String minWindow(String s, String t) {// 4、将需要判断的字串维护在hashMap中for (int i = 0; i < t.length(); i++) {ori.put(t.charAt(i), ori.getOrDefault(t.charAt(i),0) + 1);}// 5、开始遍历s串,通过快慢指针while (fast < s.length() && slow <= fast) {// 6、将s逐个维护在hashMap中cur.put(s.charAt(fast), cur.getOrDefault(s.charAt(fast), 0) + 1);// 7、当新加入字符后,需要判断是否满足最小字串请求,并且小于之前字串的长度while (check(t.length())) {// left 还没有移动,所以下面的判断不能放在 while循环中if ((fast - slow + 1) < len) {len = fast - slow + 1;resL = slow;resR = fast + 1;}// 将cur中slow下标的串-1cur.put(s.charAt(slow), cur.getOrDefault(s.charAt(slow), 0) -1);++slow;}// 循环退出条件++fast;}return resL == -1 ? "" : s.substring(resL, resR);}private boolean check(Integer len) {// 如果 fast 小于 t 的长度,直接返回 falseif (fast < len - 1) {return false;}// 遍历 ori 或者 curIterator iterator = ori.entrySet().iterator();while (iterator.hasNext()) {// 如果 cur 包含该元素,val >= ori.val 则表示成功,否则失败;Map.Entry entry = (Map.Entry)iterator.next();Character key = (Character)entry.getKey();Integer val = (Integer)entry.getValue();// 当前返回的串的个数小于目标串t的个数,说明不符合,直接退出if (cur.getOrDefault(key, 0) < val) {return false;}}return true;}
}
时间复杂度: 最坏情况下左右指针对s
的每个元素各遍历一遍,哈希表中对s
中的每个元素各插入、删除一次,对t
中的元素各插入一次。每次检查是否可行会遍历整个t
的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为C
,则渐进时间复杂度为O(C⋅∣s∣+∣t∣)
)。
空间复杂度: 这里用了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,我们设字符集大小为C
,则渐进空间复杂度为O(C)
。