算法--滑动窗口

目录

[一 认识](#一 认识)

[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;
      }

    }

相关推荐
蒙奇D索大5 分钟前
【数据结构】图解图论:度、路径、连通性,五大概念一网打尽
数据结构·考研·算法·图论·改行学it
uhakadotcom9 分钟前
2025年春招:如何使用DeepSeek + 豆包优化简历,轻松敲开心仪公司的大门
算法·面试·github
小白狮ww13 分钟前
Retinex 算法 + MATLAB 软件,高效率完成图像去雾处理
开发语言·人工智能·算法·matlab·自然语言处理·图像识别·去雾处理
trust Tomorrow1 小时前
每日一题-力扣-2278. 字母在字符串中的百分比 0331
算法·leetcode
Lecea_L2 小时前
你能在K步内赚最多的钱吗?用Java解锁最大路径收益算法(含AI场景分析)
java·人工智能·算法
Tony882 小时前
热题100 - 394. 字符串解码
java·算法
Lecea_L2 小时前
🔍 找到数组里的“节奏感”:最长等差子序列
java·算法
是Dream呀2 小时前
ResNeXt: 通过聚合残差变换增强深度神经网络
人工智能·算法
学习2年半2 小时前
53. 最大子数组和
算法
君义_noip3 小时前
信息学奥赛一本通 1524:旅游航道
c++·算法·图论·信息学奥赛