目录
一、滑动窗口的特定步骤:
二、题目解析
1、⻓度最⼩的⼦数组---点击跳转题目
3、最⼤连续 1 的个数 III----点击跳转题目
4、将 x 减到 0 的最⼩操作数----点击跳转题目
5、⽔果成篮----点击跳转题目
滑动窗口是双指针算法中细分的一种,它由暴力枚举算法优化而来,解决的是同向双指针问题;当一个题目在暴力解法中发现枚举时其实可以不用把“指针”往回走,就可以使用滑动窗口的思想来解决。也就是滑动窗口通过题目的某种性质,忽略了很多无效枚举,维护两个指针间的区间保持某种性质,从而把时间复杂度从O(n^2)优化到O(n)
只要题目的研究对象是一段连续区间或者能转化为一段连续区间,就可以考虑用滑动窗口来做,思路不清晰时可以先想题目的暴力枚举,从中优化而来
一、滑动窗口的特定步骤:
- 定义指针left、right
- 确定如何进窗口
- 判断(指针内区的某种性质进入循环)用while循环做判断
- 通过判断不断出窗口,使得指针内的区间满足性质
- 确定在哪一步更新结果
滑动窗口的代码形式是双重循环,外层是right指针遍历,内层是为了指针内区间满足某种性质而不断出窗口;虽然是双重循环,但是时间复杂度只有O(n)
二、题目解析
1、⻓度最⼩的⼦数组---点击跳转题目
由于此问题分析的对象是「⼀段连续的区间」,因此可以考虑「滑动窗⼝」的思想来解决这道题。 让滑动窗⼝满⾜:从 i 位置开始,窗⼝内所有元素的和⼩于 target (那么当窗⼝内元素之和 第⼀次⼤于等于⽬标值的时候,就是 i 位置开始,满⾜条件的最⼩⻓度)。 做法:将右端元素划⼊窗⼝中,统计出此时窗⼝内元素的和:
▪ 如果窗⼝内元素之和⼤于等于 target :更新结果,并且将左端元素划出去的同时继续判 断是否满⾜条件并更新结果(因为左端元素可能很⼩,划出去之后依旧满⾜条件)
▪ 如果窗⼝内元素之和不满⾜条件: right++ ,让下⼀个元素进⼊窗⼝。
代码:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int l = 0, r = 0;
long long sum = 0;
int len = INT_MAX;
while(r < nums.size())
{
sum += nums[r];//进窗口
while(sum >= target)//判断
{
len = min(r - l + 1,len);//更新结果
sum -= nums[l++];//出窗口
}
r++;
}
return len == INT_MAX ? 0 : len;
}
};
时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者 最多都往后移动 n 次。因此时间复杂度是 O(n)
2、⽆重复字符的最⻓⼦串----点击跳转题目
暴力思路:枚举「从每⼀个位置」开始往后,⽆重复字符的⼦串可以到达什么位置。找出其中⻓度最⼤的即可。在往后寻找⽆重复⼦串能到达的位置时,可以利⽤「哈希表」统计出字符出现的频次,来判断什么时候⼦串出现了重复元素。
优化:研究的对象依旧是⼀段连续的区间,因此继续使⽤「滑动窗⼝」思想来优化。
让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。
做法:右端元素 ch 进⼊窗⼝的时候,哈希表统计这个字符的频次:
▪ 如果这个字符出现的频次超过 1 ,说明窗⼝内有重复元素,那么就从左侧开始划出窗⼝,
直到 ch 这个元素的频次变为 1 ,然后再更新结果。
▪ 如果没有超过 1 ,说明当前窗⼝没有重复元素,可以直接更新结果
代码:
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int hash[128];//下标表示字符,数组存储字符个数,可以快速查找每个字符个数
int l = 0, r = 0;
int len = 0;
while(r < s.size())
{
hash[s[r]]++;//进窗口
while(hash[s[r]] > 1)//判断
hash[s[l++]]--;//出窗口
len = max(len,r - l + 1);//更新结果
r++;
}
return len;
}
};
3、最⼤连续 1 的个数 III----点击跳转题目
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k 个 0 嘛。 因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超 过 k 个。 比特就业课 既然是连续区间,可以考虑使⽤「滑动窗⼝」来解决问题。
滑动窗口的性质:区间内至多有k个0,维护这个性质即可
代码:
class Solution {
public:
//转化:找到最长的子数组,0的个数不超过k个
int longestOnes(vector<int>& nums, int k)
{
int l = 0, r = 0, len = 0;
int zero = 0;
while(r < nums.size())
{
if(nums[r] == 0) zero++;//进窗口
while(zero > k)//判断性质是否满足
{
if(nums[l++] == 0) zero--;//出窗口
}
len = max(len,r - l + 1);//性质满足时更新结果
r++;
}
return len;
}
};
4、将 x 减到 0 的最⼩操作数----点击跳转题目
题⽬要求的是数组「左端+右端」两段连续的和为 x 的最短数组,正难则反,我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最⻓数组。此时,就是熟悉的「滑动窗⼝」问题了
target = sum - x
滑动窗口的性质:区间内和为target
题目所求的最小操作数也就是 nums的个数减去最长子数组
代码:
class Solution {
public:
//转化:找到最长子数组,和为 sum - x
int minOperations(vector<int>& nums, int x)
{
int sum=0, target, len = -1;
// sum = accumulate(nums.begin(), nums.end(), 0);
for(auto e : nums) sum += e;
target = sum - x;
if(target < 0) return -1; //sum < x的情况不可能把x减为0
int l = 0, r = 0;
long long s = 0;//区间内的和
while(r < nums.size())
{
s += nums[r];//进窗口
while(s > target)//需要的是s等于target
{
s -= nums[l++];//出窗口
}
if(s == target) //上面循环跳出来时s可能等于或小于target
len = max(len,r - l + 1);//更新结果
r++;
}
if(len == -1) return len;
else return nums.size() - len;
}
};
5、⽔果成篮----点击跳转题目
研究的对象是⼀段连续的区间,可以使⽤「滑动窗⼝」思想来解决问题。 让滑动窗⼝满⾜:窗⼝内⽔果的种类只有两种。
做法:右端⽔果进⼊窗⼝的时候,⽤哈希表统计这个⽔果的频次。这个⽔果进来后,判断哈希表的⼤⼩:
▪ 如果⼤⼩超过 2:说明窗⼝内⽔果种类超过了两种。那么就从左侧开始依次将⽔果划出窗 ⼝,直到哈希表的⼤⼩⼩于等于 2,然后更新结果;
▪ 如果没有超过 2,说明当前窗⼝内⽔果的种类不超过两种,直接更新结果 len。
代码:
class Solution {
public:
//找出一个最长子数组,数组中最多两种水果
int totalFruit(vector<int>& fruits)
{
const int N = 1e5 + 10;
//下标表示水果种类,数组元素表示区间中这种水果放了几次
int hash[N] = {0};
int l = 0, r = 0;
int kinds = 0,len = 0;//kinds表示区间内水果种类数
while(r < fruits.size())
{
if( hash[fruits[r]]++ == 0) kinds++;
while(kinds > 2)
{
//出窗口
hash[fruits[l]]--;
if(hash[fruits[l]] == 0) kinds--;
l++;
}
len = max(len,r - l + 1);
r++;
}
return len;
}
};