在算法的数组、字符串问题中很大一部分问题都是关于子元素的筛选的,大多数都可以通过嵌套的循坏进行匹配,但是在这种方法中的时间复杂度都不可避免的到了O(n^2),而此时就可以使用滑动窗口算法,将嵌套循环转化为单循环问题。主要有以下两个常见类型的滑动窗口问题:
1.给定窗口大小
2.最小最大类问题
原理分析
举例1:给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串。(minimum-window-substring)
输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"
对于这个问题我们常见的思路就是进行匹配
for(int i = 0 ; i < S.size() ; i++)
for(int j = i + 1; j < S.size() ; j ++){
flag = true
for(int k = 0 ; k < T.size();k ++)
if(T[k] 不存在 S[i..j]中){
flag = false;
break;
}
if(flag == true && min > j-i){
min = j - i;
left = j;
right = i;
}
}
但是这种暴力算法的复杂度为O(k·n^2),如果使用动态窗口算法算法复杂度将降低到O(n+k),
对算法进行分析:
假设题目是 在 S 中找出包含 T 所有字母的第一个子串,我们很容易解决问题了,但是题目是找到最小的子串,就会存在一些问题。
1.当前窗口内可能包含了一个更小的能满足题目的窗口
2.窗口没有滑动到的位置有可能包含了一个更小的能满足题目的窗口
为了解决可能出现的问题,当我们找到第一个满足的窗口后,就从左开始缩小窗口。
如果缩小后的窗口仍满足包含 T 所有字母的要求,则当前窗口可能是最小能满足题目的窗口,储存下来之后,继续从左开始缩小窗口。
如果缩小后的窗口不能满足包含 T 所有字母的要求,则缩小窗口停止,从右边开始扩大窗口。
int left = 0;
int right = 0;
int count = T.length;
int max = Number.MAX_SAFE_INTEGER;
int res = S;
while (right < S.length) {
if (map[S[right]] > 0) {
count--;
}
map[S[right]]--;
right++;
while (count === 0) {
if (right - left < max) {
max = right - left;
res = S.slice(left, right);
}
map[s[left]]++;
if (map[s[left]] > 0) {
count++;
}
left++;
}
}
return max === Number.MAX_SAFE_INTEGER ? "" : res;
leetcode上有关滑动窗口的经典习题(题目见链接)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len = s.length();
if(len==0){
return 0;
}
int subStringLen = 0;
int left = 0;
unordered_set<char> subString;
for(int i = 0 ; i < len ; i++){
while(subString.find(s[i])!= subString.end()){
subString.erase(s[left]);
left ++;
}
subStringLen = max(subStringLen,i-left+1);
subString.insert(s[i]);
}
return subStringLen;
}
};
2.最小覆盖子串
3.至多包含两个不同字符的最长子串
4.长度最小的子数组
5.滑动窗口最大值
6.字符串的排列
7.最小区间
8. 最小窗口子序列
小结
滑动窗口本质上是一种是启发式的遍历数组问题中所有符合要求的子元素,并通过标记寻找最好的结果的算法,依然还是一种暴力解法,但通过维持一个动态窗口,可以有效的减少遍历的时间复杂度。在现实应用方面,常使用在TCP流量控制环节中。