题目链接
方法一 暴力求解(Time Limit Exceed)
算法思想:
- 枚举输入字符串s的所有子串(由于子串要覆盖t中所有字符,因此子串的长度一定大于等于字符串t的长度,此时可以节省一部分时间);
- 判断每个子串是否符合要求,子串中元素是否覆盖了字符串t中所有字符;
- 在判断每个子串的过程中,对于符合要求的子串检查其长度,记录下长度更短的子串;
时间复杂度:T(n) = O(|S|3 +|T|) 。获取子串是|S|2,检查符合要求是|S|+|T|。
空间复杂度:S(n) = O(|S|+|T|)。
C++实现
class Solution {
public:
unordered_map<char, int> tFreq; // 字符串t的频数数组
// 检查子串是否包含t中所有字符
bool check(const string &str){
unordered_map<char, int> subFreq; // 当前子串的字符频数数组
for(const auto &c: str){
++subFreq[c];
}
for(const auto &p: tFreq){
if(subFreq[p.first] < p.second){
return false;
}
}
return true;
}
string minWindow(string s, string t) {
// 初始化tFreq
int lenT = t.size();
for(const auto &c: t){
++tFreq[c];
}
// 初始化最短符合要求的子串
int len = INT_MAX;
string curSubstr = "";
// 双重遍历得到s的子串,检测是否符合题目要求,涵盖了t中所有字符
int lenS = s.size();
for(int start = 0; start < lenS; ++start){
for(int subLen = lenT; start + subLen <= lenS; ++subLen){
string tmp = s.substr(start,subLen);
if(check(tmp) && tmp.size() < len){ // 该子串符合要求且长度更小
curSubstr = tmp;
len = tmp.size();
}
}
}
return curSubstr;
}
};
运行结果如下
题目给出字符串长度最长达到105,按照这个时间复杂度,显然不是短时间能完成的。
方法二 双指针滑动窗口
暴力解法存在的问题:做了很多无用功
- 在已经找到了一个符合要求的子串后,以当前子串左边界为起点的更长的子串就不用看了;
- 判断每个子串是否覆盖了字符串t在利用滑动窗口时不需要遍历整个子串,详情见下列方法。
算法思想:利用双指针构造一个滑动窗口,滑动窗口在不断移动的过程中找到最短的包含字符串t中字符的子串。
- 滑动窗口通过右指针来扩展,每次向右移动一次,完成窗口的扩展,并且检查此时窗口内是否包含了字符串t中所有字符。
i. 如果包含,那么考虑从窗口左侧窗口减小窗口大小以求得最小子串;
ii. 如果不包含,那么继续向右扩展;- 滑动窗口规模缩减,左侧指针不断右移,随着窗口规模每减小1,如若当前窗口仍然包含字符串t中所有元素,那么说明当前窗口的子串符合要求,应同之前的子串长度作比较,长度更小则更新子串状态。循环往复,直到不再满足"窗口内元素包含字符串t中所有元素"这一要求,此时说明以r位置为尾部的子串长度已达到最小。应该继续往后扩展寻找是否有新的符合要求的子串。
时间复杂度分析:滑动窗口左右指针各遍历一遍,外加对字符串t的哈希插入,耗时O(|s|+|t|);每一次插入后都进行一次子串覆盖判定耗时O(C)【假定字符串t中字符集大小为C】,因此总的时间复杂度为T(n) = O(C|s|+|t|)。
空间复杂度分析:使用了两张哈希表,根据字符集大小为C,因此消耗空间为S(n) = O(C)
C++实现
class Solution {
public:
// tFreq:给定字符串t中元素的数量
// winFreq:表示字符串s的子串s[l,r]中包含的t中各元素的数量
unordered_map<int, int> tFreq, winFreq;
// 检查子串s[l,r]中是否涵盖了字符串t中所有字符,即比较tFreq和winFreq的元素数量
bool check(){
for(auto const &elem: tFreq){
if(winFreq[elem.first] < elem.second){
return false;
}
}
return true;
}
string minWindow(string s, string t) {
// 初始化tFreq
for(const auto &c:t){
++tFreq[c];
}
// 初始化子串的状态
int len = INT_MAX; // 初始情况满足要求的最短子串的长度设定为正无穷
int ansL = -1, ansR = -1; // 初始子串的左端和右端索引
// 初始化滑动窗口
int left = 0, right = -1; // 表示滑动窗口的左右指针
int lenS = s.size();
while(right < lenS){
// 右指针向右扩展一位,更新winFreq
if(tFreq.find(s[++right]) != tFreq.end()){ // 如果s[++right]的元素是字符串t中的元素,那么子串中该元素的数量+1
++winFreq[s[right]];
}
// 检查并更新最短子串状态
// 如果此刻的子串s[left,right]是否包含了t中所有字符,那么更新子串状态,同时调整滑动窗口左端位置
if(check()){
do{
// 缩减滑动窗口左端直到不满足"子串包含t中所有字符"的要求
if(tFreq.find(s[left]) != tFreq.end()){ // 注意:每次缩减还需要更新子串对应的count表
--winFreq[s[left]];
}
++left;
}while(check() && left<=right);
int curLen = right-left+2; // 当前满足要求的子串的长度
if(curLen < len){ // 找到了更小的子串->更新最小子串的范围及长度
len = curLen;
ansL = left-1;
ansR = right;
}
}
}
return len == INT_MAX?string():s.substr(ansL,len);
}
};
运行结果如下
方法三 滑动窗口+距离定义
算法思想:在方法二的基础上,定义滑动窗口同字符串t之间的距离,通过距离作为滑动窗口是否覆盖子串的标志,从而减少了判断的时间。定义见代码。
时间复杂度:T(n) = O(|s|+|t|),此处与字符集大小不再相关;
空间复杂度:S(n) = O(C);
C++实现
class Solution {
public:
unordered_map<int, int> tFreq, winFreq;
int distance = 0; // 表示滑动窗口内部包含了字符串t中字符的个数。窗口内增加字符时,当该字符的个数超过了在t中的个数,distance不再增加;窗口内减少字符时,若该字符数量超过了在t中的个数,则distance不减少;当distance==t中字符总个数时,即说明窗口内字符覆盖了t中所有字符。
// 检查子串s[l,r]中是否涵盖了字符串t中所有字符,即比较count和origin的元素数量
bool check(const string &t){
return distance == t.size()? true:false;
}
string minWindow(string s, string t) {
// 初始化tFreq
for(const auto &c:t){
++tFreq[c];
}
// 初始化子串的状态
int len = INT_MAX; // 初始情况满足要求的最短子串的长度设定为正无穷
int ansL = -1, ansR = -1; // 初始子串的左端和右端索引
// 初始化滑动窗口
int left = 0, right = -1; // 表示滑动窗口的左右指针
int lenS = s.size();
while(right < lenS){
// 右指针向右扩展一位,更新winFreq表和distance
if(tFreq.find(s[++right]) != tFreq.end()){ // 如果s[++r]的元素是字符串t中的元素,那么子串中该元素的数量+1
++winFreq[s[right]];
if(winFreq[s[right]] <= tFreq[s[right]]){ // 如果窗口内的该字符超过了字符串t对应字符,则说明本次增加的字符属于多余的字符,则距离保持不变
++distance; // 距离字符串t中对应字符更近一步,
}
}
// 如果此刻的滑动窗口包含了t中所有字符,那么需要缩减调整滑动窗口左端位置,找到以right结尾的最短子串,更新最短子串状态
if(check(t)){ // 大循环出发小循环
do{
// 缩减滑动窗口左端直到不满足"滑动窗口覆盖字符串t"的要求
if(tFreq.find(s[left]) != tFreq.end()){ // 如果窗口左端字符是字符串t中字符
--winFreq[s[left]];
if(winFreq[s[left]] < tFreq[s[left]]){ // 如果除去该字符后窗口内的该字符仍大于等于字符串t对应字符,则说明本次减少的字符属于多余的字符,则距离保持不变
--distance;
}
}
++left;
}while(check(t) && left<=right);
// 更新最小覆盖子串
int curLen = right-left+2; // 当前以right位置字符为结尾的最小覆盖子串的长度
if(curLen < len){ // 找到了更小的子串->更新最小子串的范围及长度
len = curLen;
ansL = left-1;
ansR = right;
}
}
// ====上面小循环也可写成如下这种形式====
// while(check(t) && left<=right){
// // 更新最小覆盖子串
// int curLen = right-left+1; // 当前以right位置字符为结尾的最小覆盖子串的长度
// if(curLen < len){ // 找到了更小的子串->更新最小子串的范围及长度
// len = curLen;
// ansL = left;
// ansR = right;
// }
// // 缩减滑动窗口左端直到不满足"滑动窗口覆盖字符串t"的要求
// if(tFreq.find(s[left]) != tFreq.end()){ // 如果窗口左端字符是字符串t中字符
// --winFreq[s[left]];
// if(winFreq[s[left]] < tFreq[s[left]]){ // 如果除去该字符后窗口内的该字符仍大于等于字符串t对应字符,则说明本次减少的字符属于多余的字符,则距离保持不变
// --distance;
// }
// }
// ++left;
// }
}
return len == INT_MAX?string():s.substr(ansL,len);
}
};
运行结果如下
同方法二对比,方法三的优化在时间上显然高效了很多,而空间上仍然保持占用不变。
方法四
未完待续
看完点个赞呗!(●'◡'●)