算法图解—最小覆盖子串

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

算法图解—最⼩覆盖⼦串
【题⽬描述】
给你⼀个字符串 s 、⼀个字符串 t 。

返回 s 中涵盖 t 所有字符的最⼩⼦串。

如果 s 中不存在涵盖 t 所有字符的⼦串,则返回空字符串 "" 。

注意:如果 s 中存在这样的⼦串,我们保证它是唯⼀的答案。

⽰例 1:
输⼊:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
⽰例 2:
输⼊:s = "a", t = "a"
输出:"a"
⽰例 3:
输⼊:s = "a", t = "bb"
输出:""
来源:⼒扣(LeetCode)第76题
链接:https:///problems/minimum-window-substring
著作权归领扣⽹络所有。

商业转载请联系官⽅授权,⾮商业转载请注明出处。

熟悉的童靴都知道,这道题属于双指针的经典题⽬:
我们称之为“滑动窗⼝”。

【题⽬分析】
我们先看看题⽬是什么意思。

本问题要求我们返回字符串 s中包含字符串 t 的全部字符的最⼩窗⼝。

我们称包含 t 的全部字母的窗⼝为「可⾏」窗⼝。

通过⽰例很容易明⽩题⽬意思,只要窗⼝⾥包含有⽬标字符串t中的左右字符,且要求是最短的。

那么什么是“滑动窗⼝”呢?
我把它⽐作家中的铝合⾦推拉窗。

对就是上图的这个东东。

在滑动窗⼝类型的问题中都会有两个指针。

⼀个⽤于「延伸」现有窗⼝的 right 指针(右侧),和⼀个⽤于「收缩」窗⼝的 left 指针(左侧)。

在任意时刻,只有⼀个指针运动,⽽另⼀个保持静⽌。

我们在 s 上滑动窗⼝,通过移动 right 指针不断扩张窗⼝。

当窗⼝包含 t 中全部所需的字符后,如果左侧能收缩,我们就收缩窗⼝(左侧指针 left)直到得到最⼩窗⼝。

有⼈会问,为什么收缩要从左侧指针⽅向?
因为,你扩张是从右侧的,当停⽌扩张时,你想想,为什么会停⽌扩张?是因为当窗⼝包含 t 中全部所需的字符了。

所有此时最右侧的字符⼀定是必须的,故要从左侧缩减,如果能的话。

【图解⽰例:参考leetCode】
作者:LeetCode-Solution
链接:https:///problems/minimum-window-substring/solution/zui-xiao-fu-gai-zi-chuan-by-leetcode-solution/
来源:⼒扣(LeetCode)
著作权归作者所有。

商业转载请联系作者获得授权,⾮商业转载请注明出处。

假设:
s = ABAACBAB
t = ABC
第⼀步:
第⼆步:从第⼀步到第四步中间少了四步即是从A->B->A->A
第三步:left 缩减⾄B,仍然是包含了ABC,但不知道是否是最短,只好记录下来left ,right,len = right - left。

第四步:继续缩减;
第五步:再次包含ABC,记录下此时的left ,right,len = len>(right - left) ? (right - left) : len。

第六步:仍然包含ABC,记录下此时的left ,right,len = len>(right - left) ? (right - left) : len。

left 继续右移。

第七步:
第⼋步:同上,记录,⽐较。

不在赘述。

第九步:
第⼗步:⾄此后,结束。

思路知道了,那么说⼀说细节:
1、如何判断当前的窗⼝包含所有 t 所需的字符呢?
这涉及到数据结构的运⽤了,我们知道Java中hashMap查找的时间复杂度是O(1)。

借此,我们可以⽤⼀个需要匹配的哈希表 need<char, int> 表⽰ t 中所有的字符以及它们的个数,⽤⼀个窗⼝匹配哈希表 windows<char, int>动态维护窗⼝中所有的字符以及它们的个数,如果这个动态表中包含 t 的哈希表中的所有字符,并且对应的个数都不⼩于 t 的哈希表中各个字符的个数,那么当前的窗⼝是「可⾏」的,即是满⾜“包含”的条件的。

2、如何判断这个动态表windows中是否包含 t 的哈希表中的所有字符,并且对应的个数都不⼩于 t 的哈希表中各个字符的个数?
可以利⽤⼀个int 变量即可,设为kind,我称之为种类,即当windows中达到need中某字符的数量时,该变量加1。

【代码实现】
//C++
class Solution {
public:
string minWindow(string s, string t) {
//if(s.size() < t.size()) return "";
unordered_map<char, int> need,windows;
for(char c:t) need[c]++;
int kind = 0;//windows中的种类
int left = 0;
int right = 0;
int len = INT_MAX;//返回长度
int start = 0;//返回起始下标
while(right < s.size()){
//current char
char c = s[right++];
if(need.count(c)){
windows[c]++;
if(windows[c] == need[c]){
kind++;
}
}
//if windows中种类齐全了,缩减左侧
while(kind == need.size()){
if(right - left < len){
start = left;
len = right - left;
}
char d = s[left++];
if(need.count(d)){
windows[d]--;
if(need[d] > windows[d]){
kind--;
}
}
}
}
return len == INT_MAX ? "" : s.substr(start,len);
}
};
【Java】
class Solution {
Map<Character, Integer> ori = new HashMap<Character, Integer>();
Map<Character, Integer> cnt = new HashMap<Character, Integer>();
public String minWindow(String s, String t) {
int tLen = t.length();
for (int i = 0; i < tLen; i++) {
char c = t.charAt(i);
ori.put(c, ori.getOrDefault(c, 0) + 1);
}
int l = 0, r = -1;
int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;
int sLen = s.length();
while (r < sLen) {
++r;
if (r < sLen && ori.containsKey(s.charAt(r))) {
cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
}
while (check() && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
ansR = l + len;
}
if (ori.containsKey(s.charAt(l))) {
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
++l;
}
}
return ansL == -1 ? "" : s.substring(ansL, ansR);
}
public boolean check() {
Iterator iter = ori.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Character key = (Character) entry.getKey();
Integer val = (Integer) entry.getValue();
if (cnt.getOrDefault(key, 0) < val) {
return false;
}
}
return true;
}
}
【复杂度分析】
时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历⼀遍,哈希表中对 s 中的每个元素各插⼊、删除⼀次,对 t 中的元素各插⼊⼀次。

每次检查是否可⾏会遍历整个 t 的哈希表,哈希表的⼤⼩与字符集的⼤⼩有关,设字符集⼤⼩为 C,则渐进时间复杂度为
O(C⋅∣s∣+∣t∣)。

空间复杂度:这⾥⽤了两张哈希表作为辅助空间,每张哈希表最多不会存放超过字符集⼤⼩的键值对,我们设字符集⼤⼩为 C ,则渐进空间复杂度为 O(C)。

Over...。

相关文档
最新文档