滑动窗口法用于求某个序列满足特定条件的连续子序列。优点是时间复杂度低。
具体使用方法为设立左右边界(具体是左闭右开还左闭右闭都行,不过要保持区间的开闭性不变),边界内是子序列,然后根据是否满足条件决定左右边界的移动,一般来说,求最长子序列时,满足条件右边界移动,不满足条件左边界移动,求最短子序列时,满足条件左边界移动,不满足条件右边界移动。有时候最关键的是怎么判断条件满足,一般我们可以用一个总量式的变量来提高判断效率。还有一个比较麻烦的点在于左右边界移动时加入删除某些元素引起的关键条件的变化。
下面看题。
第一题:209. 长度最小的子数组
这题就是比较简单的滑动窗口,通过判断滑动窗口区间内的和来控制窗口移动。
/**
* 解题思路:使用滑动窗口,设立左边界和右边界和最小长度,当当前滑动窗口小于目标值时,右边界加1,
* 当前滑动窗口大于等于目标值时若当前窗口长度小于最小长度则最小长度更新,左边界加1
* 当滑动窗口小于目标值且右边界值为序列长度减1时终止循环
* 返回最小长度。
*
*/
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int len = nums.size();
if (len == 0)
return 0;
int left = 0, right = 0, minlen = 0, s = nums[0];
while (true) {
if (s >= target) {
if ((right - left + 1) < minlen || minlen == 0)
minlen = right - left + 1;
s = s - nums[left];
left++;
}
else {
if (right == len - 1)
break;
right++;
s = s + nums[right];
}
}
return minlen;
}
};
第二题:904. 水果成篮
还是滑动窗口,不过现在是最长子序列,且关键条件的判断变得麻烦了一些。
/**
* 解题思路:滑动窗口法。左边界指向第一个采摘的树,右边界指向下一个要采摘的树,左闭右开,用一个2*2的数组两个篮子里水果的种类和数量(初始化为0),
* 第一列表示种类,第二列表示数量,用s表示当前水果数量,maxnum表示最大水果数量,开始循环,若右边界指向的要采摘的树等于二维数组中
* 某个种类的水果,则将相应数量加1,水果数量加1,maxnum据情况更新,右边界加1,否则将左边界指向的水果类的数量减1,s减1,左边界加1
* 循环终止条件为右边界等于树的数量.
* 返回maxnum
*/
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int left = 0, right = 0, s = 0, maxsum = 0;
int bucket[2][2] = {0, 0, 0, 0};
int len = fruits.size();
while (right != len) {
if (fruits[right] == bucket[0][0] || bucket[0][1] == 0) {
bucket[0][0] = fruits[right];
bucket[0][1]++;
s++;
right++;
}
else if (fruits[right] == bucket[1][0] || bucket[1][1] == 0) {
bucket[1][0] = fruits[right];
bucket[1][1]++;
s++;
right++;
}
else if (fruits[left] == bucket[0][0]) {
bucket[0][1]--;
left++;
s--;
}
else if (fruits[left] == bucket[1][0]) {
bucket[1][1]--;
left++;
s--;
}
if (s > maxsum)
maxsum = s;
}
return maxsum;
}
};
第三题:76. 最小覆盖子串
这个题虽然标的是困难,但只要有一定经验,还是比较容易能做出来,不过我花了很久时间才做出来,而且是看了网友的答案,为啥呢,主要题目说设计一个时间复杂度为O(n)的算法,我一开始和官方题解想的差不多,不过我真的觉得那种解法的复杂度不是O(n),至少是t的长度乘以n,所以想了另外一种解法,不过那种解法太麻烦了,这里也给我一个提示,如果想了一种非常麻烦的算法,一般都是想岔了,这个时候还是看看答案比较好,不然真写出来太耗时间了。
这里用的也是一种用总量也节省判断是否符合条件的思想,我觉得这个挺常见的(例如第一题总不可能在每次判断中用O(n)的时间吧所有元素加起来),当然,中间还用了哈希表,总之当查找某个东西的时间比较长,那么就要想到用哈希表。
/**
* 解题思路:我是看了官方题解才做的,我其实很早就想到了官方题解那种解法,但是我始终觉得那个时间复杂度不是O(n),后面我又看到一种解法
* 其实那种解法我也想到过,只不过没有深入想。
* 还是用滑动窗口,设立左边界右边界,还有最小序列时左右边界的位置,用一个哈希表need,还有一个记录总需量allNeed,当总需量变为0时说明满足条件
* 把左边界指向的元素踢出滑动窗口,该元素对应的need实值加1,若实值大于0了,则allNeed加1。判断是否为最小序列及是否更新答案
* 当总需量不为0时说明不满足条件,把右边界指向元素
* 包括进滑动窗口,该元素对应的need实值减1,若实值大于等于0了,则allNedd减1.循环终止条件为不满足条件且right等于s长度
* 返回答案。
*/
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> need;
int allNeed = 0;
for (auto i : t) {
if (need.find(i) != need.end())
need[i]++;
else
need[i] = 1;
allNeed++;
}
int left = 0, right = 0, lresult = 0, rresult = 0;
int len = s.length();
int minlen = 0, currlen = 0;
while (true) {
if (allNeed == 0) {
int temp = s[left];
if (need.find(temp) != need.end()) {
need[temp]++;
if (need[temp] > 0)
allNeed++;
}
if (currlen < minlen || minlen == 0) {
minlen = currlen;
lresult = left;
rresult = right;
}
currlen--;
left++;
}
else {
if (right == len)
break;
int temp = s[right];
if (need.find(temp) != need.end()) {
need[temp]--;
if (need[temp] >= 0)
allNeed--;
}
currlen++;
right++;
}
}
return s.substr(lresult, minlen);
}
};
总结
和开头的差不多,总之滑动窗口用于求复合条件的连续子序列,看是否满足条件来判断左右边界的移动。比较关键的是满足条件的判断以及左右边界移动时关键变量的变化。当然,有时候区间的定义也是比较关键的,左闭右闭还是左闭右开看哪个方便就用哪个,想好了之后用边界条件测试一下。