题目
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
示例 2:
输入:s = "a", t = "a"
输出:"a"
提示:
1 <= s.length, t.length <= 105
s 和 t 由英文字母组成
进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
思路
用两个 map<char, int>
分别统计滑动窗口和字符串 t
中各个字符出现的次数,用双指针 left
,right
遍历字符串 s
,每次先让 right
向右一格,如果窗口中的字母数量符合要求(即窗口中的子串合法)的话尝试让 left
向右收缩窗口,直到收缩到窗口不符合要求为止,继续让 right
向右扩张窗口。
时间复杂度:
O
(
C
s
+
t
)
O(Cs + t)
O(Cs+t)
最坏情况下左右指针分别遍历完 s,设字符集长度为 C,每次检查两个 map 是否符合要求需要 C,再加上最开始统计 t 的字符集需要 t
空间复杂度:
O
(
C
)
O(C)
O(C)
C++ 代码
class Solution {
public:
bool checkEqual(map<char, int>& winCnt, map<char, int>& tCnt) {
for (auto it = tCnt.begin(); it != tCnt.end(); ++it) {
if (winCnt[it->first] < it->second)
return false;
}
return true;
}
string minWindow(string s, string t) {
int sizeT = t.size(), sizeS = s.size();
map<char, int> winCnt, tCnt; // 分别记录滑动窗口和字符串t中各个字符出现的次数
for (int i = 0; i < sizeT; ++i) {
++tCnt[t[i]];
}
int minStart, minLen = INT_MAX, left = 0, right = -1;
while (right < sizeS - 1) {
++right;
if (tCnt.count(s[right]))
++winCnt[s[right]];
while (checkEqual(winCnt, tCnt)) {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minStart = left;
}
if (tCnt.count(s[left]))
--winCnt[s[left]];
++left;
}
}
return minLen == INT_MAX ? "" : s.substr(minStart, minLen);
}
};
最开始错误的思路和 C++ 代码
我觉得这个题表述有模糊的地方,我一开始写了一版代码通过了两个题中的样例,但是提交的时候这个样例:
输入:
"a"
"aa"
输出:
"a"
的意思就是各个字母的出现次数还得匹配,想了一下再用我这个思路改会很复杂,就看了题解。
class Solution {
public:
string minWindow(string s, string t) {
/* u用来记录t中所有不重复字符
为u中所有字符建立一个map<char, int> win;
int表示当前窗口中这个char最后出现的位置
用双指针left, right遍历字符串s
首先找到第一个符合条件的子串[left, right],顺便初始化win
并记录win中的最小值minRights,记录minLen=right-left,记录minStart=left
不断向右移动right,如果遇到t中的字符,按顺序更新leftSet,win
再尝试尽量右移left(这里要维护一个set<int> leftSet来找到left的下一个位置)
更新minLen和minStart
遍历结束后从s[minStart]开始输出minLen个字符即为所求
因为有这个测试样例,所以找第一个[left, right]的时候还要统计字母次数...
输入:
"a"
"aa"
输出:
"a"
*/
// 构造u,初始化win
map<char, int> win;
string u; // u用来记录t中所有不重复字符
int sizeT = t.size(), sizeS = s.size();
for (int i = 0; i < sizeT; ++i) {
if (win.count(t[i]) == 0) {
win[t[i]] = 0;
u.push_back(t[i]);
}
}
// 遍历s
int left = 0, right = 0, cntU = 0, sizeU = u.size(), minLen, minStart;
set<int> leftSet; // 最小堆
// 定位第一个合法的left,并统计信息
while (win.count(s[left]) == 0)
++left;
win[s[left]] = left;
++cntU;
leftSet.insert(left);
// 定位第一个合法的right,并统计信息
while (cntU < sizeU) {
++right;
if (win.count(s[right]) != 0) {
win[s[right]] = right;
leftSet.insert(right);
++cntU;
}
}
minLen = right - left + 1;
// right向右遍历完s
++right;
if (right >= sizeS)
return s.substr(minStart, minLen);
for (; right < sizeS; ++right) {
if (win.count(s[right]) != 0) {
leftSet.erase(win[s[right]]);
win[s[right]] = right;
leftSet.insert(win[s[right]]);
left = *(leftSet.begin());
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minStart = left;
}
}
}
return s.substr(minStart, minLen);
}
};