一、问题描述
给你两个字符串S和T,请你在S中找到包含T中全部字符的最短子串。如果没有这样一个子串,则算法返回空串;若找到这样一个子串,则可认为答案是唯一的。
二、暴力解法
1.算法
//暴力解法
string fun(string s, string t)
{
unordered_map<char, int> need;
unordered_map<char, int> temp;
//遍历字符串t,记录其中每个字符出现的次数
for (char c : t)
need[c]++;
int len = s.size()+1, right = 0, left = 0;
int minLen=t.size();//字符串T的长度也是最短子串的最小长度
for (int i = 0;i < s.size();i++)
{
for (int j = i + 1;j < s.size();j++)
{
if (j - i >= minLen)
{//只有子串长度大于或等于T的长度,才能包含T中所有的字符
int index = 0;
int count = 0;
while (index < t.size())
{//遍历字符串t,保证子串包含t中所有字符
string tempS = s.substr(i, j - i);
//使用temp记录当前子串tempS中的字符,便于和t中字符比较
for (char c : tempS)
temp[c]++;
char f = t[index];
index++;
if (temp.count(f))
count++;
else
break;//子串tempS不包含字符f,则当前子串无效,中段循环
temp.clear();//清除temp中的所有元素,为保存下一子串做准备
}
//count == minLen表明当前子串s.substr(left,j-i)中包含t中所有字符,且当前子串长度小于上一子串长度就更新子串
if (count == minLen && (j-i<len))
{
len = j - i;
left = i;
right = j;
//cout << "left=" << left << " right" << right << " len" << len << endl;
break;//此处中断的是第二次循环,因为子串s.substr(left,j-i)长度一定小于子串s.substr(left,j+1-i)的长度
}
}
}
}
return len == s.size() + 1 ? " " : s.substr(left, len);
}
``
2.分析
两层for循环遍历字符串s是为了不断获取子串s.substr(i,j-1),以便查看子串是否包含t中的全部字符(即第二层循环中while的功能),在所有符合条件的子串中选出长度最小的子串(若存在两个或两个以上的最小字串,则选取最前的子串最为答案)。算法的重要地方均在代码中已给出了注释,若仍有不懂的请留言谈论,我会尽快回复。
该算法思想直接简单,且使用了三层循环,时间复杂度必定大于O(N^2)。
注:字符串t中不可出现中重复字符
三、滑动窗口算法
1.算法思想
1):我们在字符串s中使用双指针中左、右指针技巧,初始化left=right=0,将索引左闭右开区间**[left,right)称为一个窗口
2):先不断地增加right指针扩大窗口[left,right),直到窗口中的字符串符合要求(包含t中的所有字符)。
3):此时停止增加right,转而不断增加left指针缩小窗口[left,right)**,直到窗口中的字符不再符合要求,同时每次增加left,都要更新一次结果。
4):重复第2和第3步,直到right到达字符串s的尽头。
2.算法代码
//移动窗口函数
string minWindow(string s, string t)
{
unordered_map<char, int> need, window;
//遍历字符串t,记录其中每个字符出现的次数
for (char c : t)
need[c]++;
int left = 0, right = 0;
int valid = 0;
//记录最小覆盖字串的起始索引和长度
int start = 0, len = s.size()+1;
while (right < s.size())
{
char c = s[right];
right++;//向右扩大窗口
if (need.count(c))
{
window[c]++;
if (window[c] == need[c])
valid++;//当前字符在window与need中的对应值一致,则将有效值加一
}
//valid == need.size()表示在当前窗体中已经包含need中的所有字符
while (valid == t.length())
{//进入循环,直到窗体window中不在完全包含need中的所有字符
int formedSize=right-left;
if (right - left< len)
{
start = left;//更新窗口起始位
len = right - left;//更新窗口大小
}
char c = s[left];//提取当前窗口第一个字符
left++;//窗口左端右移
if (need.count(c))
{
if (window[c]==need[c])
valid--;//当前字符在window与need中的对应值一致,则将有效值减一
window[c]--;//当前字符在need(t)中存在,便将window中的对应值减一
}
}
}
//cout << endl;
//cout << "start=" << start <<" len="<< len<< endl;
return len == s.size()+1 ? "" : s.substr(start, len);
}
3.简单分析
算法中只在while (right < s.size())中嵌套了while (valid == t.length())一个循环,属于两层循环。算法的时间复杂度明显比暴力解法的时间复杂度小。
注:该滑动窗口算法是本人在<<labuladong的算法小炒>>一书的1.7.1节中学习所得,主要代码与书中基本一致,只修改了小部分以及添加了一部分的注释。