算法详解:滑动窗口机制 - 指南

一.什么是滑动窗口

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

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

二.为什么需要滑动窗口

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

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

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

// 时间复杂度:O(n³) 或 O(n²)
bool hasDuplicate(string s, int start, int end) {
    unordered_set 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的子数组的最大平均值

double findMaxAverage(vector& 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:

[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,重复这个过程,直到遍历完整个数组

class Solution {
public:
    int minSubArrayLen(int target, vector& nums) {
        int l = 0,sums = 0;//初始化左边界以及元素和
        int minl = INT_MAX;//记录最小子数组长度
        for(int i = 0;i=target){
                minl = min(minl,i-l+1);//更新最小长度
                sums -= nums[l];//将收缩后移出子数组的元素减去
                l++;//左边界收缩
            }
        }
        //判断有没有找到最小子数组
        if(minl == INT_MAX){
            return 0;
        }
        else{
            return minl;
        }
    }
};

六.滑动窗口的综合使用

滑动窗口也可以和其他数据结构一起使用,以解决更多的类型的题目

(1)滑动窗口+哈希表

力扣3:无重复字符的最长子串

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

示例 1:

"abc"

示例 2:

"b"

示例 3:

"wke"

思路:

        用一个哈希表去记录滑动窗口中的数字,每遍历到一个新的数字时,若其在哈希表中说明重复,反之则不重复,这也是处理重复数据问题最常用的方法.  

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        //哈希表
        unordered_mapmp;
        int left = 0;//左边界
        int maxLen = 0;//最大长度
        //遍历数组
        for(int i = 0;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

class Solution {
public:
    int totalFruit(vector& fruits) {
        //初始化
        unordered_mapmp;
        int maxNum = 0;
        int left = 0;
        //遍历数组
        for(int i = 0;i2){
                //收缩左边界,将移出的种类的水果数量-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,如果发现左边界的值就是队列头部元素的值,说明最大值要被移出窗口了,就把队列头部元素删除

        由于我们维护的是单调队列,在没有更大的元素进入窗口时,删去头部元素后,新的头部元素仍然是窗口的最大值

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. 灵活:适用于各种变种问题

希望这篇指南能帮助你掌握这个强大的算法技巧!加油!!!

posted @ 2025-12-26 16:22  gccbuaa  阅读(5)  评论(0)    收藏  举报