滑动窗口
- 1.长度最小的字数组
- 2.无重复字符的最长子串
- 3.最大连续1的个数 III
- 4.将 x 减到 0 的最小操作数
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1.长度最小的字数组
题目链接:209.长度最小的字数组
题目分析:
注意题目说的是正整数数组,说明数组里面的数是大于等于0的数。因此这道题我们有一种优化的方法。
题目让找连续的子数组和大于或等于target,并且长度最小。有很多种情况,但是我们选择的是最小长度。
算法原理:
不管什么题,首先我们一定会先想到的是暴力求解,因为只有暴力求解出来了,我们就可以在暴力求解的基础上进行优化!
解法一:暴力枚举出所有的子数组的和
两层for循环,O(N^2)
注意到此时暴力枚举是有优化的。之前是每次都从前往后走一遍,但是现在定义一个left,一个right初始化都指向0下标,sum+=nums[right],++right。
等到符合要求统计一下长度,
注意题目说的是一个正整数数组,都是大于等于0的数,这个sum是呈现出递增的状态的,单调递增!。
在暴力求解中,此时right还要++,但是注意题目本来要求的就是最小长度,此时sum在加上往上走了一步的right的num[right],一定是满足sum>=target,但是len变成5了,一定不会是最终结果,因此当条件已经满足sum>=target ,right就不用动了。right后面也就不用再枚举了。
那现在让left+1,right和left指向同一下标,然后再重复上面过程,那有个问题,这段区间的和能不能直接算出来?
当然可以。现在sum=8,我只需要把让sum减去num[left],不就是现在left和right所在的区间和算出来吗。没有必要让right傻傻的回退然后重新加。因此right不动,更新sum=6.
因此我们从暴力枚举中发现两个优化:
一个是right后面不用枚举,一个left++,right不用回退,
所以我们可以使用双指针优化。
解法二:利用单调性,使用 “同向双指针” 来优化
当我们在暴力枚举的策略中发现left和right都是从左向右一个方向移动,我们就称为这两个指针叫做同向指针。同向双指针又称为滑动窗口。
什么是滑动窗口?
本质上是 “同向双指针”,left从左到右移动,right不回退,从左到右移动,用left和right一直维护这个区间的和,然后这两个指针从左向右移动的过程非常像一个窗口在这个数组里滑来滑去。
什么时候用滑动窗口?
利用单调性,用滑动窗口解决问题。
当我们发现在暴力求解时,两个指针都可以做到不回退,都是向同一个方向移动的时候,此时就可以用滑动窗口。
滑动窗口怎么用?
- 初始left=0,right=0,充当窗口左端点,右端点。用left,right标记窗口左区间,右区间。
- 进窗口(++right)
- 判断
根据判断决定是否出窗口(++left) - 更新结果
2,3都有可能会更新结果,看题目要求
进窗口,判断,出窗口一直循环,直到right超过区间长度结束,更新结果看题目要求(进窗口,出窗口都有可能),。
滑动窗口正确性
暴力枚举肯定对的,因为已经把所有子数组的情况都找出来了。虽然滑动窗口并没有把没有把所有情况都枚举出来,但是这里利用单调性,规避了很多没有必要的枚举行为。虽然没有把所有情况真正枚举出来,但是已经判断出有些子数组不是最终结果,已经把所有结果都考虑进来了,所以这种策略是跟暴力枚举是一样正确的。
滑动窗口时间复杂度
进窗口是一个循环,判断也是一个循环。两层循环套在一起。你会觉得时间复杂度O(N^2),但是不能看代码算时间复杂度,要看实际情况分析实际复杂度。实际我们只会让right向前移动,left也向前移动,即使时最坏情况,right移动到最后一个元素,lefi也移动到最后一个元素,因为总共操作次数最多n+n次 整体时间复杂度O(N)。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
// 使用滑动窗口解决问题
int n=nums.size(),sum=0,len=INT_MAX;
for(int left=0,right=0;right<n;++right)
{
sum+=nums[right];// 进窗口
while(sum>=target)// 判断
{
len=min(len,right-left+1);// 更新结果
sum-=nums[left++];// 出窗口
}
}
return len==INT_MAX?0:len;
// // 1.初始窗口
// int left=0,right=0,len=INT_MAX;
// int n=nums.size(),sum=0;
// while(right<n)//2.进窗口
// {
// sum+=nums[right];
// while(sum>=target)
// {
// //4.更新结果
// if(right-left < len)
// len=right-left;
// //3.出窗口
// sum-=nums[left];
// ++left;
// }
// ++right;
// }
// return len==INT_MAX?0:len+1;
}
};
2.无重复字符的最长子串
题目链接:3. 无重复字符的最长子串
题目分析:
子串和子数组都是连续的
算法原理:
首先还是暴力枚举,然后根据暴力枚举进行优化。
以下面为例,两层for循环,但是下面找到的结果都是我们站在上帝角度,编译器并不知到什么时候结束。一般对应判断是否有重复元素,我们都可以用哈希表来解决问题。
使用哈希表,判断是否有重复元素,比如让你判断一个数组是否有重复,或者两个数组是否有重复都可以用哈希映射!
解法一:暴力枚举+哈希表(判断字符是否重复出现)
O(N^2)
根据解法一做优化,定义一个left,right指针。当right走到有重复的元素后,已经找到一个字串,其中left到right区间每个元素都已经进入hash表。
此时left向前走一步,但是这个区间还是有重复元素,因此left要走到没有重复的区间才行,
然后这个时候以前做法是right回退然后重新往下走,但是这里left到right区间元素本来就在hash表里,因此就不需要right回退了,而是向right继续向前走。然后重复上面过程,直到right走到结尾。结束!
这不就是滑动窗口的思想吗。双向指针,left往前走,right不回退一直往前走!
解法二:利用规律,使用 “滑动窗口” 解决问题
- left=0,right=0
- 进窗口
- 判断
出窗口 - 更新结果
进窗口、判断、出窗口,更新结果是一个大循环过程。直到right到结尾循环结束。其中判断、出窗口是一个小循环。不过时间复杂度还是O(N).
注意更新结果可能在进窗口后,判断后,出窗口后,判断后任意一个地方,看题目要求
本题:
进窗口 ->-> 让字符进入哈希表
判断-> 窗口内出现重复元素
出窗口-> 从哈希表中删除该字符
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int hash[128]={0};//使用数组模拟哈希表
int n=s.size(),ret=0;
for(int left=0,right=0;right<n;++right)
{
hash[s[right]]++; // 进窗口
while(hash[s[right]] > 1) // 判断
{
hash[s[left++]]--;//出窗口
}
ret=max(ret,right-left+1);//更新结果
}
return ret;
}
};
总结一下:
利用单调性,使用 双指针 解决问题。
一般left和right,一个指向数组最左边,一个指向数组最右边,然后一次可以排除一批,再然后left++,–right,两个指针是对撞的。
这里利用单调性或者利用规律,使用 滑动窗口 解决问题
滑动窗口也使用双指针解决问题,不过left一直从前往后走,right不回退从前往后走,两个指针是同向的。因此滑动窗口本质其实是同向双指针。
3.最大连续1的个数 III
题目链接:1004. 最大连续1的个数 III
题目分析:
题目说的翻转实际上是把0变成1的意思,最多翻转K次,说明小于等于K都是可以的。
拿到题我们开始肯定想的是暴力求解。如果直接暴力求解,遇到0->1了,那下一次在遍历就有问题了。因此我们换一个思路。这道题不是让转化后最大连续1的个数吗。我们转化为:找出最长的子数组,数组里0的个数不超过K个,这个数组里面0一定能够转化成1。
算法原理:
解法一:暴力枚举+zero计数器
伪代码,两层for循环,统计zero的个数,满足zero>k,统计此时数组长度,然后重新进入循环,注意每次zero都清0
然后我们根据暴力枚举,看看有没有优化的可能。定义两个指针left,right,right走到zero>k的位置,zero=3,大于k。
按照暴力求解left++,然后right回溯然后重新往后走。但是我们发现没有必要,现在left往前走一步,你会发现,right还是停留在老位置!这个区间不用在管的!直接丢弃。
因此,让left一直走到zero<=k的位置。然后right也根本不用回溯然后在重新走,而是直接往后走就行了。
根据上面的发现,当在暴力枚举中,发现left,right是同向移动的,利用这个规律,使用滑动窗口解决问题
解法二:利用规律,使用滑动窗口
- left=0,right=0
- 进窗口
- 判断
出窗口 - 更新结果
进窗口 -> 如果是1,不理会。如果是0,计数器+1
判断 -> zero>k
出窗口 -> 如果是1,不理会。如果是0,计数器-1
更新结果:在判断之后在更新
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int n=nums.size(),len=0,zero=0;
for(int left=0,right=0;right<n;++right)
{
// 进窗口
if(nums[right] == 0)
++zero;
while(zero>k)//判断
{
if(nums[left++] == 0) //出窗口
--zero;
}
//更新结果
len=max(len,right-left+1);
}
return len;
}
};
4.将 x 减到 0 的最小操作数
题目链接:1658. 将 x 减到 0 的最小操作数
题目分析:
这道题让每次从数组左右两边移除一个数,然后就是一个新的数组,然后再从新的数组再从左右两边移除一个数。但是如果真的硬着头皮开始做,其实是很困难的。
你并不知道每次是从最左边走还是最右边找。有可能这次左边下次右边或者还是左边,情况太复杂了。
因此我们可以利用正难则反的思想
正对面解题太难,那就想对立面,换个思路。
不是每次从左右两端找一个数吗,那可能找到情况就是a+b=x,a、b什么情况都要,但是中间这个连续区间的和不也是确定的吗sum-x,也就是这道题我们转换成,找出最长的子数组长度,所有元素的和正好等于sum-x,然后数组总长减去这段子区间长度不就是问题答案吗,如果没找到说明这个数组不存在将x减到0的数,直接返回-1
解法一:暴力求解
初始left,right指向同一下标,当right走到和大于target的时候,left往前走,按照暴力求解,right要回到和left相同下标,然后right在重新往前走,直到再次走到和大于target的地方停下来,然后重复上面过程。
但是今天这里不需要right回溯,因为right回溯后重新走到下面的位置,因为left已经往前走了,这段区间的和肯定是更小了,因此就不需要right回溯了。要么right不动,要么right往后走。
同向双指针 ----> 本质就是滑动窗口
解法二:使用滑动窗口
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum = 0;
for (auto& e : nums)
sum += e;
int target = sum - x;
//细节问题
//数组里都是正整数,才能使用滑动窗口
//如果让在一个正整数数组里找和为负数的根本不可能
if (target < 0)
return -1;
sum = 0;
int len = -1;
for (int left = 0, right = 0; right < nums.size(); ++right)
{
sum += nums[right];//进窗口
while (sum > target)//判断
{
sum -= nums[left++];//出窗口
}
if (sum == target)//更新结果
{
len = max(len, right - left + 1);
}
}
if(len == -1) return len;
else return nums.size()-len;
}
};