- 最小窗口子序列-DP解法
滑动窗口是一种双指针技巧的算法框架, 其算法时间复杂度较低。
下面是leetcode中常见的几道可以使用滑动窗口解决的题目。
一、最小覆盖字串
题目链接:https://leetcode-cn.com/problems/minimum-window-substring/
题目要求在S(source) 中找到包含T(target)中全部字母的一个字串,顺序无所谓,要求这个字串的最短长度。
暴力解法,代码大概如下:
for(int i=0;i<s.size();i++)
for(int j=i+1,j<s.size();j++)
if(s[i:j] 包含t中所有字母)
更新答案
思路很简单,但是算法时间复杂度为O(n^2).
滑动窗口的思路如下:
- 在字符串S中使用双指针,初始化left = right = 0, 将索引闭区间[left, right]称为一个窗口
- 首先不断增加right,直到窗口内的字符串符合要求,包含了T中所有字符
- 此时,停止增加right,减少left直到窗口不再满足条件,每次减少的同时更新结果
- 重复第2、3 步直到right到达字符串S的末尾
简单理解就是,第二步先找到一个可行解,然后第三步优化这个可行解,不断重复第二三步直到找到最终解。
解题代码:
string minWindow(string s, string t) {
// 特殊情况
if(t.length()>s.length() || t=="") return "";
// 滑动窗口
int l=0,r=0;
// 数组代替hash_map 记录字符出现的次数
int array_s[256]={0};
int array_t[256]={0};
// 利用match记录匹配上的字符数目
int match = 0;
int sum = 0;
int start = 0, len = 0;
for(auto c:t) {
// 记录待匹配字符的数目
if(array_t[c]==0) sum++;
array_t[c]++;
}
while(r<s.length())
{
array_s[s[r]]++;
// 字符出现次数相等,匹配上一个字符
if(array_s[s[r]]== array_t[s[r]]) match++;
r++;
while(match == sum)
{
if(len==0 || r-l<len)
{
start = l;
// r在之前已经+1, 这里的长度不需要加1
len = r-l;
}
array_s[s[l]]--;
if(array_s[s[l]] < array_t[s[l]]) match--;
l++;
}
}
// 利用substr函数进行截取
return len==0?"":s.substr(start,len);
}
这个算法的时间复杂度为O(2M+N), M,N分别是字符串S以及T的长度。while内部left与right最多都遍历到M,也就是O(2M).
二、找到字符串中所有字母异位词
题目链接 https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/
题目要求在字符串S中找到非空字符串P的字母异位词的字串,并返回字串的起始索引。
与最小覆盖字串思路一致,同样适用一个数组array_p记录字符串P中字符的出现次数,然后利用滑动窗口的思想找到一个满足array_p中所有字符次数的可行解,并优化它,然后移动继续寻找其他可行解,重复上述过程。
代码如下:
vector<int> findAnagrams(string s, string p) {
int l=0,r=0;
vector<int> vc;
// 只有小写字母
int array_p[26]={0};
int array_s[26]={0};
// 使用sum统计需要考虑的字符个数
int match = 0, sum = 0;
for(auto c:p)
{
if(array_p[c-'a']==0) sum++;
array_p[c-'a']++;
}
while(r<s.length())
{
array_s[s[r]-'a']++;
// 当某个字符出现次数满足时,匹配字符数+1
if(array_s[s[r]-'a'] == array_p[s[r]-'a']) match++;
r++;
// 找到一个符合条件的窗口 然后优化
while(match==sum)
{
// 只有长度与p长度相等时 才会push
if(r-l==p.size()) vc.push_back(l);
array_s[s[l]-'a']--;
if(array_p[s[l]-'a']>array_s[s[l]-'a']) match--;
l++;
}
}
return vc;
}
三、 无重复字符的最长子串
题目链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
遇到字串问题,首先应该想到的就是滑动窗口技巧,类似之前的思路,使用一个数组记录字符出现的次数,当某个字符出现次数大于1时,收缩窗口直到该字符出现次数等于1。
值得注意的是,因为题目需要求解最长字串,因此应该收缩窗口得到可行解之后,才去更新答案。
这道题目与一般的滑动窗口解题不太一样,一般的滑动窗口是先找到一个解,当找到可行解时优化可行解,优化过程中更新答案。而本题是在通过内层的while循环找到一个解,然后通过内层while循环找到可行解,结束循环之后更新答案。
int lengthOfLongestSubstring(string s){
int set[256] = {0};
int left=0,right=0,ans=0;
while(right<s.size())
{
char c = s[right];
set[c]++;
right++;
while(set[c]>1)
{
set[s[left]]--;
left++;
}
ans = max(ans, right-left);
}
return ans;
}
总结
通过上面三道题目,总结下滑动窗口思想:
int left = 0, right = 0;
while(right<s.size())
{
window.add(s[right]);
right++;
while(valid){
window.remove(s[left]);
left++;
}
}
}
一般来说window使用hash表或者数组记录即可。
稍微麻烦的是valid条件,很多复杂的代码都是为了实现这个条件的实时更新。