算法详解:滑动窗口机制

一.什么是滑动窗口

想象一下,你正在透过一个固定大小的窗口观察一条长长的数据序列,这个窗口可以左右滑动,让你看到序列的不同部分------这就是滑动窗口算法的直观理解。

滑动窗口是一种用于处理数组/字符串子区间问题 的优化技巧。它通过维护一个窗口(通常是连续的子数组或子字符串),在遍历过程中动态地调整窗口的左右边界,从而高效地解决问题。

二.为什么需要滑动窗口

让我们先来看一个经典问题:

给定一个字符串,找出其中不含有重复字符的最长子串的长度。

暴力解法:枚举所有子串,检查是否重复

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 maxSlidingWindow(vector& nums, int k) { if(nums.size()<=1){ return nums; } dequeque;//双端队列,可以删除头部也可以删除尾部 que.push_back(nums[0]); vectorvec;//结果数组 //遍历前k个元素并维护单调队列 for(int i = 1;ique.back()){ que.pop_back(); } que.push_back(nums[i]);//双端队列的插入 } //将第一个滑动窗口的最大值存入结果数组中 vec.push_back(que.front()); //记录左边界 int left = 0; //从第k个元素开始遍历 for(int i = k;ique.back()){ que.pop_back(); } que.push_back(nums[i]); //判断最大值会不会随着左边界收缩而被移出窗口,如果会就把最大值从队列中删除 if(nums[left]==que.front()){ que.pop_front(); } left++;//左边界收缩 vec.push_back(que.front());//将最大值存入结果数组 } return vec; } }; ``` ## 七.总结 滑动窗口算法之所以强大,在于它: 1. **高效**:将O(n²)优化到O(n) 2. **直观**:符合人类的思维习惯 3. **通用**:有固定的模板可以套用 4. **灵活**:适用于各种变种问题 希望这篇指南能帮助你掌握这个强大的算法技巧!加油!!!

相关推荐
淀粉肠kk34 分钟前
【C++】封装红黑树实现Mymap和Myset
数据结构·c++
Zero-Talent35 分钟前
“栈” 算法
算法
橘子编程35 分钟前
经典排序算法全解析
java·算法·排序算法
waeng_luo36 分钟前
【鸿蒙开发实战】智能数据洞察服务:待回礼分析与关系维护建议算法
算法·ai编程·鸿蒙
风筝在晴天搁浅36 分钟前
代码随想录 279.完全平方数
算法
不穿格子的程序员39 分钟前
从零开始刷算法——字串与区间类经典题:前缀和 + 单调队列双杀
算法·前缀和·哈希表·双向队列·单调队列
坚持就完事了40 分钟前
十大排序算法
数据结构·算法·排序算法
wefg11 小时前
【C++】IO流
开发语言·c++
im_AMBER1 小时前
Leetcode 63 定长子串中元音的最大数目
c++·笔记·学习·算法·leetcode