滑动窗口

概述

定义

滑动窗口是一种常用的算法技术,通过维护移动窗口状态,避免重复计算,将算法复杂度从 \(O(n^2)\) 降低到 \(O(n)\),属于空间换时间。

  • 窗口:数组或字符串中的一个连续区间;
  • 滑动:类似窗口在数组或字符串中移动,对于固定窗口,左右边界会同时向右移动,对于可变窗口,边界移动是独立的,具体有以下表现:
    • 右边界向右扩展,窗口变大,包含更多元素
    • 左边界向右收缩,窗口变小,排除不符合条件的元素

术语说明

  • 子数组:数组中连续的元素序列,如 [1,2,3,4] 中的 [2,3]
  • 子串:字符串中连续的字符序列,如"abcd"中的"bc"
  • 子序列:保持相对顺序但不要求连续的序列,如 [1,2,3,4] 中的 [1,3,4]

注意:滑动窗口算法仅适用于子数组和子串问题,不适用于子序列问题。

实现步骤

标准流程

flowchart TD A[初始化: left=0, right=0] --> B[右指针向右移动] B --> C[将新元素加入窗口] C --> D[更新窗口状态] D --> E{窗口满足条件?} E -->|是| F[记录结果] E -->|否| G[左指针向右移动] F --> H{right到达末尾?} G --> I[将元素移出窗口] I --> J[更新窗口状态] J --> K{窗口重新满足条件?} K -->|否| G K -->|是| H H -->|否| B H -->|是| L[返回结果] style A fill:#e1f5fe style F fill:#c8e6c9 style L fill:#ffcdd2 style E fill:#fff3e0 style K fill:#fff3e0 style H fill:#fff3e0

指针移动策略

  1. 同步窗口(同步移动)

    • 适用场景:窗口大小固定的问题,如求长度为 k 的子数组的最大和等问题。
    • 移动方式:左右指针保持固定距离,同时向右移动。
    • 实现特点:通常使用单层循环,无需内层循环控制左指针。
  2. 动态窗口(条件收缩)

    • 适用场景:窗口大小可变,需要满足特定约束条件,如包含特定元素个数的子数组问题。
    • 移动方式:右指针逐步扩展,左指针在违反条件时连续收缩。
    • 实现特点:外层循环控制右指针,内层循环控制左指针收缩。
  3. 双指针收缩(优化收缩)

    • 适用场景:寻找满足条件的最优解,如包含特定元素个数的问题。
    • 移动方式:右指针扩展寻找可行解,左指针收缩优化解质量。
    • 实现特点:在满足条件时收缩左指针,寻找更优解。

复杂度分析

时间复杂度:\(O(n)\)

  • 每个元素最多被左指针和右指针各访问一次
  • 虽然存在嵌套循环,但内层循环的总执行次数不超过 n 次
  • 关键洞察:左指针在整个算法过程中最多移动 n 次

空间复杂度:\(O(k)\)

  • k 为维护窗口状态所需的额外空间
  • 对于计数问题:\(O(字符集大小)\)\(O(元素种类数)\)
  • 对于求和问题:\(O(1)\)

适用场景

常见问题特征

  • 连续的子数组/子串:明确提到“连续”、“子数组”、“子串”等关键字,但如果是允许跳跃的选择元素,则不适用。
  • 存在某种单调性或可逆性:窗口扩大时某个指标(如元素个数、种类数、总和)发生可预测的变化,且这种变化在窗口收缩时可以有效逆转。
  • 状态可维护性:能够通过简单的数据结构(如哈希表、计数器)以 \(O(1)\)时间复杂度维护窗口状态的增删操作。
  • 存在优化空间:暴力解法存在大量重复计算,有优化潜力。

典型问题类型

第一阶段:固定窗口

  • 使用技巧
    • 左右指针同步移动,保持固定距离
    • 使用"移除旧元素,添加新元素"的更新策略
    • 通常只需单层循环,实现相对简单
  • 示例算法
    • 简单:LeetCode 643 - 子数组最大平均数 I
    • 中等:LeetCode 1456 - 定长子串中元音的最大数目
    • 中等:LeetCode 1052 - 爱生气的书店老板

第二阶段:动态窗口-计数约束

  • 使用技巧
    • 右指针扩展窗口,左指针收缩窗口
    • 使用哈希表记录元素出现次数
    • 当窗口违反约束时,连续收缩直到满足条件
  • 示例算法
    • 中等:LeetCode 904 - 水果成篮
    • 中等:LeetCode 3 - 无重复字符的最长子串
    • 中等:LeetCode 424 - 替换后的最长重复字符

第三阶段:动态窗口-和值约束

  • 使用技巧
    • 利用正数数组的单调性(窗口扩大 → 和增大)
    • 当和超过目标时收缩左边界
    • 注意处理负数情况的特殊性
  • 示例算法
    • 中等:LeetCode 209 - 长度最小的子数组
    • 中等:LeetCode 930 - 和相同的二元子数组
    • 困难:LeetCode 862 - 和至少为 K 的最短子数组

第四阶段:优化型窗口

  • 使用技巧
    • 先扩展窗口找到可行解
    • 在满足条件的基础上收缩窗口寻找最优解
    • 使用多个哈希表或计数器维护复杂状态
  • 示例算法
    • 中等:LeetCode 438 - 找到字符串中所有字母异位词
    • 中等:LeetCode 567 - 字符串的排列
    • 困难:LeetCode 76 - 最小覆盖子串

常见错误

  1. 无限循环

    • 原因:忘记移动右指针
    • 解决:确保每次外层循环都有移动右指针,如 right++
  2. 数组越界

    • 原因:指针移动时未检查边界
    • 解决:添加边界检查,如 left < len(arr) && right < len(arr)
  3. 状态维护错误

    • 原因:添加/删除元素时哈希表更新不正确
    • 解决:仔细检查计数的增减和删除逻辑
  4. 结果更新时机错误

    • 原因:在错误的位置更新最优解
    • 解决:明确何时窗口有效,何时需要记录结果
posted @ 2025-08-06 00:50  505donkey  阅读(93)  评论(0)    收藏  举报