给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
注意:
-
对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 -
如果
s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入: s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
1.如何判断t
中的每个元素全部被包含?
先确定t
中的种类、个数。想到map<char,int>。
map<char, int> m;
int count = 0;//个数
for (int i = 0; i < t.size(); i++) {
if (m.find(t[i]) == m.end()) {//没有
m.insert(make_pair(t[i], 1));
}
else {//存在了,更新value的大小
m[t[i]]++;
}
}
//打印测试
for (map<char, int>::iterator it = m.begin(); it != m.end(); it++) {
cout << "key = " << it->first << " value = " << it->second << endl;
}
//优化:
for (int i = 0; i < t.size(); i++) {//记录t中的元素种类及个数
m[t[i]]++;
}
2.定义 最小字串 的起点start
、长度len
;左指针、右指针;以及一个需要的map集合来代表动态窗口的字符元素、个数;
int left = 0;int right = 0;int len = INT_MAX;
unordered_map <char, int> cnt;
3.滑动窗口
我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针,一个用于「延伸」现有窗口的r指针,和一个用于「收缩」窗口的 l指针。在任意时刻,只有一个指针运动,而另一个保持静止。我们在 s上滑动窗口,通过移动r指针不断扩张窗口。当窗口包含 t全部所需的字符后,如果能收缩,我们就收缩窗口直到得到最小窗口。
最小覆盖字串就是在左指针移动过程中出现的。
class Solution {
public:
string minWindow(string s, string t) {
int left = 0;
int count = 0;//记录检索元素种类
int start = 0;//最小覆盖字串的起始位置
int len = INT_MAX;//最小覆盖字串的长度
map<char, int> m,cnt;//m代表t中的元素种类;cnt代表滑动窗口中的元素种类
for (int i = 0; i < t.size(); i++) {//记录t中的元素种类及个数
m[t[i]]++;
}
for (int right = 0; right < s.size(); right++) {
if ( m.find(s[right]) == m.end() ) {//不是t中的元素
cout << s[right] << " 不是t中的元素 " << endl;
continue;
}
//是
if (cnt[s[right]] < m[s[right]]){
count++;//滑动窗口中的元素个数 < t中的个数就更新count
}
cnt[s[right]]++;
while (left <= right && count == t.size()) {//找到一组
if (right - left + 1 < len) {
len = right - left + 1;
start = left;//更新起始位置
}
char L = s[left];
if(m[L] == 0){
left++;
continue;
}
if (m[L] == cnt[L]) {//当前左指针对应的元素是需要的元素
count--;//更新count
}
cnt[L]--;//只要滑动窗口内当前元素是需要的元素,就--
left++;
}
}
return len == INT_MAX ? "" : s.substr(start,len);
}
};
网友的思路:用数组存放元素种类,个数;每一个字符元素转换为ASCII码,对应各元素。
力扣提交报越界错误,不知道什么原因。
string minWindow2(string s, string t) {
if (s.empty() || s == "" || t.empty() || t == "" || s.size() < t.size()) {
return "";
}
// 维护两个数组,记录已有字符串指定字符的出现次数,和目标字符串指定字符的出现次数
//ASCII表总长128
int need[128];
int have[128];
//将目标字符串指定字符的出现次数记录
for (int i = 0; i < t.size(); i++) {
need[t[i]]++;
}
//分别为左指针,右指针,最小长度(初始值为一定不可达到的长度)
//已有字符串中目标字符串指定字符的出现总频次以及最小覆盖子串在原字符串中的起始位置
int left = 0, right = 0, min = INT32_MAX, count = 0, start = 0;
while (right < s.size()) {
char r = s[right];
//说明该字符不被目标字符串需要,此时有两种情况
// 1.循环刚开始,那么直接移动右指针即可,不需要做多余判断
// 2.循环已经开始一段时间,此处又有两种情况
// 2.1 上一次条件不满足,已有字符串指定字符出现次数不满足目标字符串指定字符出现次数,那么此时
// 如果该字符还不被目标字符串需要,就不需要进行多余判断,右指针移动即可
// 2.2 左指针已经移动完毕,那么此时就相当于循环刚开始,同理直接移动右指针
if (need[r] == 0) {
right++;
continue;
}
//当且仅当已有字符串目标字符出现的次数小于目标字符串字符的出现次数时,count才会+1
//是为了后续能直接判断已有字符串是否已经包含了目标字符串的所有字符,不需要挨个比对字符出现的次数
if (have[r] < need[r]) {
count++;
}
//已有字符串中目标字符出现的次数+1
have[r]++;
//移动右指针
right++;
//当且仅当已有字符串已经包含了所有目标字符串的字符,且出现频次一定大于或等于指定频次
while (count == t.size())
{
//挡窗口的长度比已有的最短值小时,更改最小值,并记录起始位置
if (right - left + 1 < min) {
min = right - left + 1;
start = left;
}
char l = s[left];
//如果左边即将要去掉的字符不被目标字符串需要,那么不需要多余判断,直接可以移动左指针
if (need[l] == 0) {
left++;
continue;
}
//如果左边即将要去掉的字符被目标字符串需要,且出现的频次正好等于指定频次,那么如果去掉了这个字符,
//就不满足覆盖子串的条件,此时要破坏循环条件跳出循环,即控制目标字符串指定字符的出现总频次(count)-1
if (have[l] == need[l]) {
count--;
}
//已有字符串中目标字符出现的次数-1
have[l]--;
//移动指针
left++;
}
}
if (min == INT32_MAX) {
return "";
}
cout << start << " " << start + min << endl;
return s.substr(start, start + min);
}
力扣动图比较直观