目录
[一 认识](#一 认识)
[1 滑动窗口的核心思想](#1 滑动窗口的核心思想)
[2 滑动窗口的适用场景](#2 滑动窗口的适用场景)
[3 滑动窗口的两种模式](#3 滑动窗口的两种模式)
[4 滑动窗口的复杂度分析](#4 滑动窗口的复杂度分析)
[5 滑动窗口的常见陷阱](#5 滑动窗口的常见陷阱)
[二 满足条件的最小区间](#二 满足条件的最小区间)
[三 滑动窗口的最值问题](#三 滑动窗口的最值问题)
一 认识
1 滑动窗口的核心思想
窗口定义
- 用两个指针left(左边界)和right(右边界)表示窗口范围[left,right]
- 初始时窗口可能为空(如left=right=0),或根据问题初始化
窗口移动规则
- 扩展右边界:探索更大的区间,寻找可能的解
- 收缩左边界:在满足条件时缩小窗口,尝试找到更优解
状态维护
- 使用哈希表、数组等数据结构记录窗口内的元素状态(如出现次数,频率等)
- 动态更新这种状态,避免重复计算
2 滑动窗口的适用场景
|--------------------|-----------------------------|---------------------|
| 问题类型 | 经典例题 | 关键特征 |
| 寻找满足条件的最短子区间 | 洛谷 P1638、LeetCode 76.最小覆盖子串 | 需要覆盖所有目标元素,且区间尽可能短 |
| 寻找满足条件的最长子区间 | LeetCode 3.无重复字符的最长子串 | 要求窗口内元素满足特定条件(如无重复) |
| 统计固定窗口内的最大值/频率 | LeetCode 239.滑动窗口最大值 | 窗口大小固定,快速计算窗口内属性 |
3 滑动窗口的两种模式
3.1 固定大小的窗口
窗口长度固定,每次移动时整体右移一位
int left = 0, right = 0;
// 初始化窗口
while (right < n) {
// 扩展右边界
right++;
// 当窗口达到固定大小时
if (right - left + 1 == k) {
// 处理当前窗口
// ...
// 收缩左边界 这样下次右边界才可以++ 保持窗口的固定长度
left++;
}
}
3.2 可变大小的窗口
窗口长度动态变化,根据条件调整左右边界
int left = 0, right = 0;
while (right < n) {
// 扩展右边界,并更新状态
// ...
right++;
// 当窗口满足条件时,尝试收缩左边界
while (窗口满足条件) {
// 更新最优解
// ...
// 收缩左边界,并更新状态
left++;
}
}
4 滑动窗口的复杂度分析
时间复杂度:O(n)
每个元素最多被左右指针各访问一次,总体操作次数为2n
5 滑动窗口的常见陷阱
1 指针移动顺序
- 先扩展右边界,再收缩左边界,顺序不可颠倒
2 状态更新滞后
- 在移动指针后需立即更新
3 重复解处理
- 当多个解长度相同时,需根据题目要求找出所求解
二 满足条件的最小区间

思路:
-
要找到包括所有画家的一个区间,我们需要用两个指针,一个指向起点,一个指向终点,然后判断该范围是否包含所有画家
-
维护一个count数组记录L-R区间内,看到一个画家画的次数,变量see记录当前窗口的不同画家数
-
左指针初始为0,右指针初始为1,右指针不断向右扩展,每次遇到一个新的画家时,see++
-
当已经看到所有的画家时,去缩短左边界,不断让count数组中左边对应的画家的画数-1,以此找到最小的区间,当画数为0时,see需要-1
package Optimize;
import java.util.LinkedList;
import java.util.Scanner;public class P1638 {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[]authors=new int[n+1];
for (int i = 1; i <= n; i++) {
authors[i]=scanner.nextInt();
}
int see=0;
int[]count=new int[m+1];
int l=0;
int minLen=Integer.MAX_VALUE;
int ansL=1;int ansR=n;
for (int r=1; r <=n ; r++) {
if (count[authors[r]]==0){
see++;
}
count[authors[r]]++;
// 当窗口包含所有画师时,尝试收缩左边界
while (see==m){
if (r-l+1<minLen){
minLen=r-l+1;
ansL=l;
ansR=r;
}
// 让左边界的画对应的画家值-1
count[authors[l]]--;
if (count[authors[l]]==0){
see--;
}
l++;
}
}
System.out.println(ansL+" "+ansR);
}
}
三 滑动窗口的最值问题

思路:
- 要求一个滑动区间内的最大值和最小值,如果采用暴力,枚举起点和终点,遍历获取最大值和最小值,时间复杂度为O(nk),题目中数据量为1e6,只有在线性情况下才能AC,所以要采用滑动窗口算法,滑动窗口在处理边界元素时只有O(1)的复杂度
- 定义一个数组来模拟队列,result数组来保存滑动窗口内的最值,当窗口第一次满足长度为k的位置时下标为k-1,所以用来保存最值的队列的长度为n-1-(k-1)+1=n-k+1
- 定义一个pos数组来记录队列中元素的下标,可以用来判断队首元素是否已过期(在窗口之外)和判断队尾元素的值是否大于(或小于)要插入元素的值,从而来判断单调性
- 每次循环时,拿到右边界元素,然后开始处理队列已有元素
-
- 判断是否满足单调性,将队尾元素和当前元素比较,如果破坏了单调性,移除队尾元素
- 判断队首元素是否过期,因为窗口大小固定,当右边界放入元素时,左边界也应该移动,拿到队首元素在原队列的pos下标,判断是否在窗口之外,在则head++,最后确保head变量指向队列中有效的最值元素
- 当队列元素等于窗口大小时,将队列的队首元素保存在result数组中
细节:
-
单调队列是递增或递减的,我们用队首元素来记录最值
-
例如:单调递增队列,那么队首元素是最小值,假设目前对列里已有元素 1 2 ,那么再插入当前元素-1时,会破坏递增的性质,那么就要删除当前队列里<=-1的元素,即将1 2删除,此时队列里只有-1,满足单调性
package Optimize;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;public class P1886 {
public static void main(String[] args) throws IOException {
StreamTokenizer st=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
st.nextToken();
int n= (int)st.nval;
st.nextToken();
int k= (int)st.nval;
int[]number=new int[n];
for (int i = 0; i <n ; i++) {
st.nextToken();
number[i]= (int)st.nval;
}
int[] min = getWindowValues(number, n, k, true);
int[] max = getWindowValues(number, n, k, false);
StringBuilder sb=new StringBuilder();
for (int ele : min) {
sb.append(ele).append(' ');
}
System.out.println(sb);
sb=new StringBuilder();
for (int ele : max) {
sb.append(ele).append(' ');
}
System.out.println(sb);
}private static int[] getWindowValues(int[] number, int n, int k, boolean isMin) { int[] result = new int[n - k + 1]; int[] pos = new int[n]; // 存储下标 int head = 0, tail = -1; for (int i = 0; i < n; i++) { // 维护队列单调性 if (isMin) { // 当前值 <= 队列中最后一个的值 弹出队尾 while (head <= tail && number[i] <= number[pos[tail]]) tail--; } else { // 当前值 >= 队列中最后一个的值 弹出队尾 while (head <= tail && number[i] >= number[pos[tail]]) tail--; } pos[++tail] = i; // 存储下标 // 移除过期元素 (超出窗口范围) while (pos[head] < i - k + 1) { head++; } // 窗口形成后记录结果 if (i >= k - 1) { // 下标 k-1之后,队列长度都为k result[i - k + 1] = number[pos[head]]; } } return result; }
}