← 返回博客列表

一、什么是滑动窗口

滑动窗口(Sliding Window)是双指针的一种特殊形态:两个指针 left、right 同向运动,夹在它们之间的连续子数组/子串就像一个"窗口",通过 right 右扩和 left 右缩来调整窗口的位置与大小,从而找到满足条件的解。

一句话定义
滑动窗口 = 同向双指针 + "右扩窗口,左缩到再次合法"的循环模式。

它的典型应用是"在数组/字符串中找满足某种条件的连续子区段"——求最长、最短、恰好 k 个、和等于某值等。如果用暴力枚举所有子区段是 O(n²) 甚至 O(n³),用滑动窗口可以降到 O(n)。

1.1 两种窗口

类型 窗口大小 移动方式 典型应用
定长窗口 固定为 k right 每步右移 1,left 同步右移 1 固定长度的最大值/平均值/异或
变长窗口 动态变化 right 右扩直到不合法,left 右缩到再次合法 最长/最短子串、和 ≥ k 的最短区间
⚠ 与普通双指针的区别
滑动窗口本质是双指针,但更强调"窗口内维护某种聚合量"(如和、计数、最值、哈希)。判断题目用对撞双指针还是滑动窗口,关键看:是否在无序数组上找连续子区段——是则滑动窗口;在有序数组上找两数关系——是则对撞。

二、核心原理与图解

理解滑动窗口的关键,是理解"窗口为什么是合法的"。right 主动扩张让窗口变大,当窗口不满足条件时 left 被动收缩来恢复合法性——这种"扩张-收缩"的交替运动保证每次操作后窗口都有意义。

2.1 定长窗口过程图

以数组 [1, 3, -1, 5, 2, 7] 求所有长度为 3 的子数组的最大值为例。窗口大小固定为 3,每步整体右移一格。

定长窗口:长度 3 的子数组最大值 数组: 1 3 -1 5 2 7 窗口 1:[0, 2],覆盖 [1, 3, -1],最大 = 3 L R 窗口 2:[1, 3],覆盖 [3, -1, 5],最大 = 5 L R 窗口 3:[2, 4],覆盖 [-1, 5, 2],最大 = 5 L R 窗口 4:[3, 5],覆盖 [5, 2, 7],最大 = 7 L R 结果:[3, 5, 5, 7] 窗口整体右移,进一个出一个,关键在"如何 O(1) 维护窗口最大值"
图 1:定长窗口在数组上滑动过程

2.2 变长窗口扩张收缩过程图

变长窗口更复杂:right 主动右扩,当窗口不合法时 left 右缩。以"和 ≥ 7 的最短子数组"为例,数组 [2, 3, 1, 2, 4, 3]。

变长窗口:扩张 → 命中 → 收缩 → 再扩张 数组: 2 3 1 2 4 3 状态 1(扩张):L=0, R=2,窗口 [2,3,1],sum = 6 < 7 不满足 → right 右扩 L R 状态 2(命中):L=0, R=3,窗口 [2,3,1,2],sum = 8 ≥ 7 满足!记录长度 4,开始 left 收缩 L R 状态 3(收缩):L=1, R=3,窗口 [3,1,2],sum = 6 < 7 不满足 → 停止收缩,right 继续右扩 L R 状态 4(命中):L=1, R=4,窗口 [3,1,2,4],sum = 10 ≥ 7 长度 4(不更优)→ left 继续收缩 L R 最终最优:L=4, R=5,窗口 [4,3],sum = 7 ≥ 7,长度 2 L R 答案:最短长度 = 2 right 单向右扩,left 只在命中后右缩——每个元素被左右各访问一次
图 2:变长窗口的"扩张-收缩"交替过程

2.3 与双指针关系图

滑动窗口和双指针并非互斥,滑动窗口是同向双指针的子集。下图直观展示它们的关系。

滑动窗口与双指针的关系 双指针(大类) 滑动窗口 同向 + 维护聚合量 对撞指针 相向运动 有序数组 快慢指针 同向异速 链表 同向特例 滑动窗口是双指针的特例——同向运动 + 窗口内维护聚合量(和/计数/最值)
图 3:滑动窗口与对撞、快慢指针的归属关系

2.4 复杂度分析

⏱ 时间复杂度:O(n)
虽然 while 循环里嵌了 while,但left 和 right 各最多右移 n 次(同向运动),总移动次数不超过 2n。注意 left 不会回退,所以不是 O(n²)。

💾 空间复杂度:O(k) 或 O(1)
变长窗口如果用哈希表维护字符计数是 O(字符集大小);定长窗口通常 O(1)。单调队列优化时是 O(k)。

三、通用模板

滑动窗口的核心是"窗口内维护什么、收缩条件是什么"。下面给出定长和变长两种骨架。

3.1 变长窗口模板(找最短)

// 变长窗口通用模板:找满足条件的最短子数组
int minSubArrayLen(int target, const vector<int>& nums) {
    int n = (int)nums.size();
    int left = 0, sum = 0, ans = INT_MAX;
    for (int right = 0; right < n; ++right) {
        sum += nums[right];          // 右扩:进入窗口
        while (sum >= target) {      // 满足条件 → 尝试收缩
            ans = min(ans, right - left + 1);
            sum -= nums[left++];     // 左缩:移出窗口
        }
    }
    return ans == INT_MAX ? 0 : ans;
}
变长窗口三要点
1. right 是主循环——for 驱动 right 右扩,每次进入窗口更新聚合量
2. while 收缩条件——满足条件时持续左缩到不满足,每次收缩更新答案
3. 找最长还是最短决定 while 位置——找最短:满足时收缩;找最长:不满足时收缩

3.2 定长窗口模板

// 定长窗口通用模板:长度为 k 的子数组最大值
vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
    vector<int> res;
    deque<int> dq;  // 存下标,对应值单调递减
    for (int i = 0; i < (int)nums.size(); ++i) {
        // 1. 队首出窗
        if (!dq.empty() && dq.front() == i - k) dq.pop_front();
        // 2. 队尾维护单调性
        while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
        dq.push_back(i);
        // 3. 窗口已满 → 队首即最大
        if (i >= k - 1) res.push_back(nums[dq.front()]);
    }
    return res;
}
定长窗口三要点
1. 窗口大小固定——通常 i - left + 1 == k 时记答案
2. 进一个出一个——right 右扩时同步检查 left 是否要出窗
3. 聚合量 O(1) 更新——和用加减;最值用单调队列;异或用 XOR 性质

四、示例一:长度最小的子数组

题目(LeetCode 209):给定一个正整数数组 nums 和目标 target,找出和 ≥ target 的长度最小的连续子数组。不存在返回 0。

4.1 示例

输入:nums = [2,3,1,2,4,3],target = 7
输出:2(子数组 [4,3])

长度最小的子数组:在 [2,3,1,2,4,3] 中找和 ≥ 7 数组: 2 3 1 2 4 3 第 1 次命中:[2,3,1,2] 和 = 8 ≥ 7,长度 4 L R 收缩到不满足:[3,1,2] 和 = 6 < 7,right 继续扩 L R 第 2 次命中:[1,2,4] 和 = 7 ≥ 7,长度 3 L R 最优命中:[4,3] 和 = 7 ≥ 7,长度 2 L R 最终答案:2 每次命中后立即收缩,确保不会错过更短的最优解
图 4:长度最小的子数组——变长窗口求解过程

4.2 C++ 代码

#include <vector>
#include <climits>
using namespace std;

int minSubArrayLen(int target, const vector<int>& nums) {
    int n = (int)nums.size();
    int left = 0, sum = 0, ans = INT_MAX;
    for (int right = 0; right < n; ++right) {
        sum += nums[right];            // 右扩:nums[right] 进窗口
        while (sum >= target) {        // 满足条件 → 持续收缩求更短
            ans = min(ans, right - left + 1);
            sum -= nums[left++];       // 左缩:nums[left] 出窗口
        }
    }
    return ans == INT_MAX ? 0 : ans;
}

注意"正整数"这个条件保证了 sum 单调——right 扩 sum 必增、left 缩 sum 必减,所以收缩不会"先满足再满足"无限循环。如果数组含负数,此模板失效,需用前缀和 + 单调队列。

五、示例二:无重复字符的最长子串

题目(LeetCode 3):给定字符串 s,找出不含重复字符的最长子串的长度。

5.1 思路:哈希维护窗口字符计数

窗口合法条件是"窗口内无重复字符"。用哈希表记录窗口内每个字符出现次数。right 右扩加入字符后,如果该字符出现次数 > 1,说明有重复——left 持续右缩直到该字符次数恢复为 1。

无重复字符的最长子串:s = "abcabcbb" 字符串: a b c a b c b b 阶段 1:窗口 "abc",无重复,长度 3 L R 阶段 2:right 进 'a',重复 → left 缩过第一个 'a' 窗口 "bca",无重复,长度 3 L R 阶段 3:right 进 'b',重复 → left 缩过第一个 'b' 窗口 "cab",无重复,长度 3 L R 最终:所有阶段最大长度都是 3 答案:3 每个字符进窗口触发检查,有重复就左缩到无重复——每次循环结束窗口都合法
图 5:无重复字符的最长子串——变长窗口扩张与收缩

5.2 C++ 代码

#include <string>
#include <unordered_map>
using namespace std;

int lengthOfLongestSubstring(const string& s) {
    unordered_map<char, int> cnt;  // 窗口内字符计数
    int left = 0, ans = 0;
    for (int right = 0; right < (int)s.size(); ++right) {
        char c = s[right];
        cnt[c]++;                          // 右扩:字符进窗口
        while (cnt[c] > 1) {             // 出现重复 → 左缩
            cnt[s[left]]--;                // 字符出窗口
            left++;
        }
        ans = max(ans, right - left + 1);  // 每次循环结束窗口必合法
    }
    return ans;
}
💡 优化思路
用 unordered_map 是 O(字符集) 空间。如果已知字符集(如 ASCII 128 字符),可换 int cnt[128] = {0} 把哈希改成数组,常数更小。
进一步:用 map[char] = index 直接记录字符上次出现位置,命中重复时直接把 left 跳到上次位置 + 1,省去 while。

六、示例三:滑动窗口最大值

题目(LeetCode 239):给定数组 nums 和窗口大小 k,窗口从最左端滑动到最右端,返回每个窗口内的最大值。

6.1 思路:定长窗口 + 单调队列

定长窗口每次只进出各一个元素,最大值如果在窗口内仍存活就直接复用;如果出窗的是最大值,要能 O(1) 拿到次大值——这正是单调队列的拿手好戏。队列存下标,对应值单调递减,队首永远是当前窗口最大。

滑动窗口最大值:nums=[1,3,-1,5,2,7], k=3 数组: 1 3 -1 5 2 7 窗口 [1,3,-1]:队 = [3, -1],最大 3 L R 窗口 [3,-1,5]:5 入队,弹掉所有 ≤ 5 的,队 = [5],最大 5 L R 窗口 [-1,5,2]:2 入队,队 = [5, 2],最大 5 L R 窗口 [5,2,7]:7 入队,弹掉所有 ≤ 7 的,队 = [7],最大 7 L R 结果:[3, 5, 5, 7] 队首存活就复用、出窗就 pop_front、入窗弹尾维护单调——三步走完成 O(1) 取最大
图 6:定长窗口 + 单调队列求窗口最大值

6.2 C++ 代码

#include <vector>
#include <deque>
using namespace std;

vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
    vector<int> res;
    deque<int> dq;  // 存下标,对应值从队首到队尾单调递减

    for (int i = 0; i < (int)nums.size(); ++i) {
        // 1. 队首出窗:下标 ≤ i-k 已不在窗口内
        while (!dq.empty() && dq.front() <= i - k) {
            dq.pop_front();
        }
        // 2. 队尾维护单调性:弹掉所有 ≤ 当前值的下标
        while (!dq.empty() && nums[dq.back()] <= nums[i]) {
            dq.pop_back();
        }
        dq.push_back(i);
        // 3. 窗口已满:队首即最大值
        if (i >= k - 1) {
            res.push_back(nums[dq.front()]);
        }
    }
    return res;
}
💡 三个关键不变量
1. 队列存下标而非值——既能比较值又能判断是否出窗
2. 队首永远是窗口最大——单调递减保证
3. 每个元素入队出队各一次——总 O(n)

七、总结与适用场景

滑动窗口核心要点

维度 要点
本质 同向双指针 + 窗口内维护聚合量,"扩张-收缩"交替运动
时间复杂度 O(n)——left、right 各最多移动 n 次
空间复杂度 O(1) 或 O(字符集/窗口大小)
识别信号 "连续子数组/子串""最长/最短""恰好 k 个""和 ≥ target"
常见变体 定长窗口、变长窗口、单调队列优化、哈希维护计数

典型题目清单

🎯 记忆口诀
"同向双指维护量,右扩左缩两不忘;定长进出各一个,变长合法才记录。"
这四句覆盖了滑动窗口的两大形态:定长看"进出同步 + 聚合 O(1) 更新",变长看"右扩到不合法、左缩到再合法、每次循环结束都更新答案"。