现在来学习"滑动窗口"这一算法思想。
至于什么是"滑动窗口"呢?简单来说就是同向双指针
;现在来通过题目来了解什么是"滑动窗口"
一、长度最小的子数组
题目链接:长度最小的子数组
题目解析
先来看题目,给了一个数组nums
和一个整数target
,让我们找到nums
的一个满足条件(条件:子数组的和大于target
)的最长子数组。
算法思路
首先第一次看到子数组/子串
问题的算法题,能想到的思路就肯定是暴力枚举
了;现在就先来看暴力枚举如何解决的。
暴力枚举
我们依次枚举整个数组的每一个子数组,并判断是否满足条件;满足条件且长度小于最总结果,就更新最终结果。
上面意思呢?
暴力解法的大致思路如上述所示;现在来看一下暴力枚举
有哪些操作是不必要的
- 当
right
第一次找到满足条件的位置并更新结果后,left++
,right
从left
位置再重新遍历这个操作感觉有些多余了,因为我们
right
遍历时第一次遇到满足条件的就停了下来;如果
right
从left++
后的位置开始再遍历到上次满足条件位置的位置,这一个遍历过程很多余;
什么意思呢?
就以暴力枚举中的示例来说:
right
第一次遍历到下标为3
位置恰好满足条件;此时区间是[0,3]
,我们更新结果之后,left++
right
从下标为1
的位置开始再次遍历;我们直到
[0,3]
区间刚满足条件,那区间[1,1]
和[1,2]
是一定不满足条件的[1,3]
才有可能满足条件。
那这样我们就可以想办法省略这个不必要的步骤:
找到满足条件的区间,并更新结果并lef++
后,如何去解决right
重新遍历的问题?
解决问题,我们要找到问题的本质:
**暴力枚举
**中为什么要重新进行遍历,本质问题就是我们不知道left++
后,区间[left,right]
的和;(所以暴力枚举才会进行重新遍历)
那我们现在定义一个变量
sum
来实时记录区间[left,right]
的和,这样不就不需要重新遍历了吗。我们只需要在
right
向后遍历和left
更新时,顺手更新一下sum
的值就可以了。
滑动窗口
其实滑动窗口就是优化了暴力枚举解法中不必要的部分
知道了如何去解决暴力枚举
中不必要的问题,现在来实现
通过上图所示推到,我们的想法是可行的,现在来看整体的一个思路
滑动窗口,为什么称为滑动窗口?
就是
right
和left
在遍历更新的过程中维护了一段区间[left , right]
,这个区间像窗口一样在数组中滑动。
现在来看实现这个问题的一个整体思路:
- 进窗口:让
right
向右遍历,更新区间[left , right]
的和sum
;称为进窗口
(就是让right
遍历到的元素进入(窗口)区间内。- 出窗口:如果当前区间满足条件(区间
和
大于target
),就更新结果,然后让left++
,并更新区间[left , right]
的和;重复上述操作直到区间不满足条件。- 更新结果:至于什么时候更新结果,每一道题都不一样,根据题目中的要求再决定什么时候更新结果。
代码实现
class Solution {
public:int minSubArrayLen(int target, vector<int>& nums) {int n = nums.size();int ret = n+1;int l=0,r=0;int sum =0;while(l<n){//进窗口sum += nums[l++];//出窗口while(sum>=target){int x= l-r;if(x < ret)ret = x;sum-=nums[r++];}}if(ret == n+1)return 0;return ret;}
};
二、无重复字符的最长子串
题目链接:无重复字符的最长子串
题目解析
题目要求找出来给定字符串的所有子串中,不包含重复字符的最长子串;(也是子串问题)
s
字符串中的字符由英文字母、数字、符号和空格组成。
算法思路
这里首先还是来看暴力解法
枚举出来所有不包含重复字符的子串
在其中找到最长的然后返回。
这里先来看一下如何处理重复字符的问题:
这里可以使用一个
hash
表,其中记录每一个字符出现的次数;这样使用
left
和right
双指针遍历的时候,遍历到hash[s[right]] > 1
就代表当前区间内存在重复字符。
当然这里所有数组来模拟hash
就可以了。
现在来看使用滑动窗口
如何去解决。
进窗口:首先
right
向右遍历,遍历过程中hash[s[right]]++
更新当前字符出现的次数;出窗口:当
hash[s[right]] >1
时就表示当前区间不满足条件,就要left++
并且hash[s[left]]--
更新区间内字符出现的字符;直到hash[s[right]]<=1
。更新结果:这里每一次出完窗口就表示找到了一个满足条件的区间,所以出完窗口之后更新结果即可。(这里在
right
刚开始遍历,每一次出完窗口,不一定会进入出窗口,但是也会更新结果,不会遗漏)。
代码实现
这里写代码时注意,我们数组模拟hash
,数组大概开辟128
就差不多够了。
class Solution {
public:int lengthOfLongestSubstring(string s) {int hash[128]={0};int n = s.size();int left = 0, right = 0;int ret = 0;while(right < n){//进窗口hash[s[right]]++;while(hash[s[right]]>1)hash[s[left++]]--;ret = max(ret,right-left+1);right++;}return ret;}
};
三、最大连续1的个数 III
题目链接:最大连续1的个数 III
题目解析
题目给定一个数组
nums
,和k
;其中nums
中每一个数字不是1
就是0
。
k
表示最多可以反转(0
变1
)的次数。要求我们返回操作之后,数组中连续
1
的最大个数。
什么意思呢?
就是我们最多可以将k
个数组中的0
变成1
,然后我们要找出进行变换操作后数组中连续1
的最大个数。
算法思路
这里,做出来本道题的关键就在于如何处理这个最多k
次0
变1
的问题。
这里我们真的要将0
变成1
?
显然是不可以的,这里数组有很多子数组,变了之后如何接着进行呢?
所以我们不能这样操作,就只能另辟蹊径:
这道题也是最长子数组问题,那也是要用滑动窗口的;我们想一想如何将其变成这样的问题?
很显然,这里题目要求最多可以进行
k
次变换,我们不能进行真的变化操作。就只能在原数组中找到一个子数组(其中
0
的个数不超过k
);
这样这个问题就变成了我们熟悉的找子数组的问题,这里条件就是区间内0
的个数不超过k
。
所以思路就简单明了了。
这里定义一个变量
zero
来记录区间[left , right]
内0
的个数。入窗口:
right
向右遍历,遇到0
就更新zero
。出窗口:如果
zero>k
就表示当前区间内不满足条件了,就进行出窗口操作;如果nums[left] == 0
,就更新zero
。更新结果:这里更新结果还是在出窗口之后,出窗口之后区间是满足条件的并且每次入窗口之后不一定会出窗口,这样更新就不会漏掉每一种情况。
代码实现
class Solution {
public:int longestOnes(vector<int>& nums, int k) {int n = nums.size();int left = 0, right =0;int zero = 0;int ret = 0;while(right<n){//入窗口if(nums[right++] == 0)zero++;//出窗口while(zero>k){if(nums[left++]==0)zero--;}//更新结果ret = max(ret,right - left);}return ret;}
};
这里第一次接触到滑动窗口,感觉有一点点抽象;
这里滑动窗口其实就是同向双指针,只是在遍历的过程中维护了一段区间就像一个窗口一样在数组中滑动。