一.什么是滑动窗口
想象一下,你正在透过一个固定大小的窗口观察一条长长的数据序列,这个窗口可以左右滑动,让你看到序列的不同部分------这就是滑动窗口算法的直观理解。
滑动窗口是一种用于处理数组/字符串子区间问题 的优化技巧。它通过维护一个窗口(通常是连续的子数组或子字符串),在遍历过程中动态地调整窗口的左右边界,从而高效地解决问题。
二.为什么需要滑动窗口
让我们先来看一个经典问题:
给定一个字符串,找出其中不含有重复字符的最长子串的长度。
暴力解法:枚举所有子串,检查是否重复
cpp
// 时间复杂度:O(n³) 或 O(n²)
bool hasDuplicate(string s, int start, int end) {
unordered_set<char> seen;
for (int i = start; i <= end; i++) {
if (seen.count(s[i])) return true;
seen.insert(s[i]);
}
return false;
}
int bruteForce(string s) {
int maxLen = 0;
for (int i = 0; i < s.length(); i++) {
for (int j = i; j < s.length(); j++) {
if (!hasDuplicate(s, i, j)) {
maxLen = max(maxLen, j - i + 1);
}
}
}
return maxLen;
}
这种解法在长字符串面前会变得极其缓慢。而滑动窗口算法可以在**O(n)**时间内解决这个问题
三.核心思想
滑动窗口通过维护一个窗口(连续的子数组/子字符串),在遍历过程中动态调整窗口的左右边界,避免重复计算。
窗口中可以直接访问到的值一般就是目标值,将嵌套多层for循环问题转化为单次遍历,将时间复杂度从 O(n²) 降低到 O(n)。
四.使用场景
(1)关于连续子数组/子字符串的
(2)要求找到满足某些条件的最长/最短子区间
(3)统计满足条件的子区间个数
五.滑动窗口的类型
滑动窗口也分为两种类型,一种是窗口定长的,还有一种是窗口长度可变的
(1)定长窗口
例题:大小为k的子数组的最大平均值
cpp
double findMaxAverage(vector<int>& nums, int k) {
double windowSum = 0;
// 初始化第一个窗口
for (int i = 0; i < k; i++) {
windowSum += nums[i];
}
double maxSum = windowSum;
// 滑动窗口
for (int i = k; i < nums.size(); i++) {
//每次移动时将左边界元素减去,将右边界元素加上
windowSum += nums[i] - nums[i - k];
//判断最大值
maxSum = max(maxSum, windowSum);
}
return maxSum / k;
}
(2)不定长窗口:
不定长窗口相对定长窗口来说更加灵活,可以动态的变化窗口大小,满足多种情况,但是也更需要判断何时收缩,可能因为一个小小的误判,就会导致整个窗口中的元素变为无效
力扣209:长度最小的子数组
给定一个含有 n个正整数的数组和一个正整数 target。
找出该数组中满足其总和大于等于target的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。** 如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
思路:
动态维护一个滑动窗口,当窗口内元素和小于target时,移动右边界,扩大窗口,当窗口内元素和大于等于target时,就尝试收缩左边界,直到元素和再次小于target,重复这个过程,直到遍历完整个数组
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int l = 0,sums = 0;//初始化左边界以及元素和
int minl = INT_MAX;//记录最小子数组长度
for(int i = 0;i<nums.size();i++){//子数组右边界
sums+=nums[i];//扩张右边界
//当元素和大于target时,持续收缩窗口,直到元素和再次小于target
while(sums>=target){
minl = min(minl,i-l+1);//更新最小长度
sums -= nums[l];//将收缩后移出子数组的元素减去
l++;//左边界收缩
}
}
//判断有没有找到最小子数组
if(minl == INT_MAX){
return 0;
}
else{
return minl;
}
}
};
六.滑动窗口的综合使用
滑动窗口也可以和其他数据结构一起使用,以解决更多的类型的题目
(1)滑动窗口+哈希表
力扣3:无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
思路:
用一个哈希表去记录滑动窗口中的数字,每遍历到一个新的数字时,若其在哈希表中说明重复,反之则不重复,这也是处理重复数据问题最常用的方法.
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//哈希表
unordered_map<char,int>mp;
int left = 0;//左边界
int maxLen = 0;//最大长度
//遍历数组
for(int i = 0;i<s.size();i++){
//如果新遍历到的元素在哈希表中
//并且在哈希表中的索引比左边界大
//说明在滑动窗口中
if(mp.find(s[i])!=mp.end()&&mp[s[i]]>=left){
//将左边界直接更新到该元素最后出现位置的后一位,避免多次多余判断
left = mp[s[i]]+1;
}
//将数据加入哈希表中
mp[s[i]]=i;
maxLen = max(maxLen,(i-left+1));//更新最大长度
}
return maxLen;
}
};
力扣904:水果成篮
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。
示例 1:
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。
示例 2:
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
示例 3:
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。
思路:
使用一个哈希表去存储篮子中已有的水果种类,当大于2时,持续收缩左边界,直到种类重新小于2
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits) {
//初始化
unordered_map<int,int>mp;
int maxNum = 0;
int left = 0;
//遍历数组
for(int i = 0;i<fruits.size();i++){
//将对应水果的种类数量+1
mp[fruits[i]]++;
//如果种类大于2
while(mp.size()>2){
//收缩左边界,将移出的种类的水果数量-1
mp[fruits[left]]--;
//更新左边界
left++;
//当某种水果数量为0时,删除这种种类
if(mp[fruits[left]]==0){
mp.erase(fruits[left]);
}
}
//记录满足要求的窗口的长度
maxNum = max(maxNum,(i-left+1));
}
return maxNum;
}
};
(2)滑动窗口+特殊队列
力扣239:滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:滑动窗口的位置 最大值
1 3 -1\] -3 5 3 6 7 3
1 \[3 -1 -3\] 5 3 6 7 3
1 3 \[-1 -3 5\] 3 6 7 5
1 3 -1 \[-3 5 3\] 6 7 5
1 3 -1 -3 \[5 3 6\] 7 6
1 3 -1 -3 5 \[3 6 7\] 7
**思路:**
通过维护一个单调双端队列(deque)来达到快速找到最大值的目的
先遍历前k个元素,用一个单调队列找到最大值,并将最大值存入结果数组中.
再从第k个元素开始遍历,每次遍历都将该数存入队列中,并维护队列的单调性,确保队列中的头部元素即为该窗口的最大值
通过一个left变量控制滑动窗口的左边界,每次循环将其+1,如果发现左边界的值就是队列头部元素的值,说明最大值要被移出窗口了,就把队列头部元素删除
由于我们维护的是单调队列,在没有更大的元素进入窗口时,删去头部元素后,新的头部元素仍然是窗口的最大值
```cpp
class Solution {
public:
vector