多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

滑动窗口算法从原理到模板:固定窗口、可变窗口与单调队列优化

滑动窗口算法从原理到模板:固定窗口、可变窗口与单调队列优化 如果说算法题里有什么是“背过模板还得跪”的滑动窗口绝对算一个。很多朋友刷题的时候都遇到过这种情况明明把模板抄下来了也知道left和right两个指针怎么挪但题目稍微一变就晕——比如窗口什么时候收缩收缩到什么时候停窗口内的数据怎么维护一旦这几个问题想不清楚代码就写成死循环或者答案差一位。这篇文章我就以实际做题和工程里的使用经验为背景把“优选算法——滑动窗口”这件事彻底拆开从最朴素的暴力枚举讲起到固定窗口、可变窗口、单调队列优化再看它在真实场景里的影子最后把实践中踩过的坑和排查思路一并整理出来。1. 滑动窗口的本质先搞懂“窗口”和“滑动”这两个动作1.1 从暴力枚举开始为什么要“框”出一段连续区间滑动窗口解决的问题几乎都有一个共同特征操作对象是数组或字符串上的一段连续区间。比如“长度为 k 的子数组的最大和”“包含某些字符的最短子串”“乘积小于 k 的连续子数组个数”这些问题的关键词是“连续”而不是任意组合。我最早学这类题的时候第一反应就是穷举。两层循环外层定起点内层定终点把所有连续子数组全部扫一遍求最大、求最小、计数。这种做法本身没错但它最大的问题在于当数组长度为n时连续子数组的数量是n(n1)/2而每个子数组可能还要再去算一次和、算一次乘积、数一次字符频次整体复杂度往往到O(n^2)甚至O(n^3)。数据量小的时候无所谓一到10^5级别的输入就跑不动了。那“窗口”是怎么来的你可以把数组想象成一条放在传送带上的长纸带用一个固定宽度的取景框去看纸带上的内容。取景框从头移动到尾每挪动一次只取走最前面的一个元素、加入一个新元素框内其余部分完全不动。这样一来每一帧的画面也就是窗口的内容不需要重新计算全部只需要在上一次的基础上做一个“加新减旧”的微调。这就是窗口思想的核心收益用增量计算替代重复计算。1.2 复杂度如何从 O(n*k) 降到 O(n)很多教程直接告诉你“滑动窗口是 O(n)”但不解释为什么导致初学者只记住了结论遇到具体问题还是不会分析。我换一个更直观的角度来解释。假设数组长度是n1000000窗口长度k1000。暴力方法对每个可能起点都重新扫一遍后 1000 个元素总共大概要扫描1000000*100010^9次这个量级用普通电脑跑起来一秒内基本没戏。滑动窗口的思路是窗口从数组头部开始每次右边界向右移动一格左边也跟着向右移动一格每次移动后窗口内只新增一个元素、移除一个元素求“窗口和”时就只需要做一次加法和一次减法全程扫描一遍数组就结束了总运算量大约就是n次级别的常数倍也就是10^6这个量级。有人可能会问窗口每次移动时内部的“状态”比如和、哈希计数、最大值都不需要重新计算吗答案是如果这个状态满足“可增量维护”的条件——即新状态可以由旧状态加上新元素、去掉旧元素得到——那就不需要重新计算。这也是为什么滑动窗口题里最常用到的数据结构不外乎整数类型的累加值、哈希表/数组频次、双端队列等等。搞明白这个原理再看后面那些模板和代码你就知道每一行到底在干嘛了。在实际做题和面试中我认为“复杂度”这件事从来不是背公式而是能说清楚“为什么少了那么多重复计算”。当你把这层逻辑想透后面再怎么变形都不慌。2. 固定窗口模板最简单也最容易抄错的形态2.1 一段可以直接落地的固定窗口框架固定窗口是所有滑动窗口里最友好的入门形态。所谓“固定”就是窗口长度始终等于k每次右边界扩展一格左边界也跟进一步始终保持窗口大小为k。典型题目就是“长度为 k 的连续子数组最大平均值”。我下面给出一段 Python 参考实现注释写得很细可以当成一个通用的起步模板def find_max_average(nums, k): n len(nums) if n k: return 0.0 # 先统计第一个窗口的和 current_sum sum(nums[0:k]) max_sum current_sum # 窗口向右滑动 for i in range(k, n): # 加入新元素移除最左边元素 current_sum nums[i] - nums[i - k] # 记录最大值 if current_sum max_sum: max_sum current_sum return max_sum / k这段代码的关键操作只有一行current_sum nums[i] - nums[i - k]。nums[i]是窗口右边界新纳入的元素nums[i - k]是窗口左边界正要被挤出去的元素。窗口每移动一步不需要再重新加一遍 k 个数字这就是上一节说的增量维护。固定窗口的适用面其实比很多人想的宽凡是“找一段长度固定的连续区间满足某个条件”“求所有长度恰好为 k 的窗口内的某种统计量”都能用。比如求长度为 k 的子数组最大平均数、子数组最大和、每个滑动窗口的平均值甚至某些“窗口内不同字符个数”的问题只要统计量能快速更新都可以套这个骨架。2.2 固定窗口的三个隐蔽细节写模板容易写对不容易。我总结三个新手特别容易翻车的地方。第一个是第一个窗口的初始化。很多人直接让左右指针从 0 开始然后一边扩展一边判断长度结果发现循环逻辑特别别扭边界条件反复出错。更省心的方法是先单独算第一个窗口就像上面的代码里先用sum(nums[0:k])做一个初始化然后再写滑动循环。这样主循环里就只需要处理“窗口已经成形之后的移动”清晰很多。第二个是先加还是先减的顺序问题。在固定窗口里因为窗口长度不变所以current_sum nums[i]和current_sum - nums[i-k]的顺序其实不影响最终结果但如果你把窗口状态更新放在“记录结果”之后就会错过第一个窗口或者最后一个窗口的结果。建议形成一个固定习惯先更新窗口状态再记录/更新答案。顺序一致了调试的时候就不容易乱。第三个是窗口长度不足 k 的情况。比如数组长度小于 k理论上根本不存在一个合法的固定窗口这时候直接返回什么值得看题目约定。有些题要求返回-1有些返回0最怕的是你没做这个判断然后去访问nums[k]导致数组越界。我见过不少人因为这个问题在面试里栽跟头说到底就是“先想清楚边界再写代码”。还有一个容易被忽略的点如果固定窗口总长度很大用sum(nums[0:k])每次都对前 k 个数求和在极端情况下比如 k 接近 n倒也不是不行但更好的做法是先用一个for循环把前 k 项累加出来甚至在读入数据时同步维护前缀和后续所有窗口的和都可以用前缀和直接相减得到。3. 可变窗口真正的难点是“什么时候缩”3.1 “右扩左缩”的基本套路一个模板应对八成题目固定窗口练熟之后真正的分水岭是可变窗口。所谓可变就是窗口长度不固定右指针不断向前走左指针在特定条件下才向前收缩从而得到“某个约束条件下最优的连续子数组/子串”。这类题最典型的场景是求“最长/最短满足条件的子数组/子串”。比如“无重复字符的最长子串”“和小于 target 的最长子数组”“包含所有目标字符的最短子串”。它们的通用解法可以浓缩成一个套路初始化left 0right 0以及一个用于表示窗口状态的变量如哈希表、计数器、累加和。外层循环right从 0 到 n-1 一直向右扩展不断把nums[right]纳入窗口状态。内层循环在“窗口状态不满足约束”或者“已经满足要求需要寻找更优解”时移动left收缩窗口同时把移出的元素从状态里剔除。每次窗口状态合法时尝试更新答案。这个套路看起来简单但真正容易出问题的是第 3 步的“收缩条件”。我把它分为两类来理解。第一类是超过约束就收缩比如“找到数组和小于 target 的最长子数组”。当窗口内的和大于等于 target 时说明当前窗口不满足要求必须把左边界往右移同时减去对应的值直到重新满足“和小于 target”。这种收缩是为了让窗口回到合法状态然后继续尝试找更长的窗口。第二类是已达目标后尝试更优解比如“包含所有目标字符的最短子串”。当窗口已经包含了全部所需字符时题目要求最短所以这时候记下当前长度然后主动从左边缩小窗口看看缩小之后还满不满足“包含全部目标字符”。如果缩小后依然满足说明找到了更短的答案如果不满足就继续扩展右边界。这样一来每一个合法窗口都不会被漏掉而且能在 O(n) 时间内覆盖所有可能的边界组合。区别清楚了再写代码思路会顺很多。很多人卡在“什么时候更新答案”上——其实答案只应该在窗口处于合法状态时更新。如果你在窗口不合法时也去更新结果就是答案被污染。3.2 用“无重复字符的最长子串”完整拆解一次过程空谈模板有点抽象我们选一道最经典的题目“无重复字符的最长子串”LeetCode 3来逐步拆解。原始问题描述很简单给定一个字符串请找出其中不含有重复字符的最长子串的长度。先想清楚状态怎么维护。我们要知道窗口内有哪些字符、以及它们各自出现了几次。这里用哈希表freq记录每个字符在当前窗口中的出现频次每次右指针扩展时把新字符计数加一如果发现某个字符计数大于 1就说明窗口里有重复字符需要收缩左边界。具体过程是这样的s abcabcbb left 0 right 0 freq {} max_len 0 right 0, 字符 a, freq[a] 1, 窗口合法, max_len 1 right 1, 字符 b, freq[b] 1, 窗口合法, max_len 2 right 2, 字符 c, freq[c] 1, 窗口合法, max_len 3 right 3, 字符 a, freq[a] 2, 发现重复 - 循环收缩 left: 移除 s[left]a, freq[a] 1, left 1, 此时窗口内字符是 abc窗口内 a 的频次为1合法 - max_len max(3, right-left13) 3 right 4, 字符 b, freq[b] 2, 发现重复 - 收缩 left: 移除 s[left]b, freq[b]1, left2, 窗口内字符 abc合法 - max_len 3 right 5, 字符 c, freq[c] 2, 发现重复 - 收缩 left: 移除 s[left]c, freq[c]1, left3, 窗口内字符 bc合法 - max_len 3 right 6, 字符 b, freq[b]2, 发现重复 - 收缩 left: 移除 s[left]b, freq[b]1, left4, 窗口内字符 cb合法 - max_len 3 right 7, 字符 b, freq[b]2, 发现重复 - 收缩 left: 移除 s[left]c, freq[c]0, left5, 窗口内字符 b合法 - 收缩 left: 移除 s[left]b, freq[b]1, left6, 窗口内字符 b合法 - max_len 3最终答案是3。这一步一步推下来你会发现窗口的收缩并不是随便乱缩的而是要缩到“当前窗口状态重新满足条件”为止。注意在right7时我们连续收缩了两次左边界说明内层循环必须是while而不是if——这是可变窗口里一个极其容易忽略的细节。代码可以写成这样def length_of_longest_substring(s: str) - int: freq {} left 0 max_len 0 for right, ch in enumerate(s): freq[ch] freq.get(ch, 0) 1 # 只要窗口内有重复字符就不断收缩左边界 while freq[ch] 1: left_char s[left] freq[left_char] - 1 left 1 max_len max(max_len, right - left 1) return max_len这里用freq[ch] 1作为收缩条件其实隐含了一个事实引起重复的必然是刚加入的ch。所以收缩时可以只盯着ch的频次直到它恢复为 1而不用每次检查整个哈希表。这也是实际做这类题时一个很实用的优化点。3.3 再拆一个“最小覆盖子串”问题加深理解“最小覆盖子串”LeetCode 76是最能检验可变窗口掌握程度的题。题目要求在字符串s中找出包含字符串t所有字符的最短子串。注意这里说的是“包含所有字符且数量不少于 t 中的数量”不要求顺序一致。我习惯用两个计数来维护状态一个是need记录 t 中每个字符还需要多少个另一个是formed表示当前窗口中“已经达到 t 要求”的字符种类数。当formed等于 t 中不同字符种类数时说明当前窗口已经是一个合法覆盖子串接下来就尽量收缩左边界来找更短答案。核心代码如下def min_window(s: str, t: str) - str: from collections import Counter need Counter(t) # required 多少种字符需要被覆盖 required len(need) formed 0 left 0 ans (float(inf), 0, 0) window {} for right, ch in enumerate(s): window[ch] window.get(ch, 0) 1 if ch in need and window[ch] need[ch]: formed 1 # 当窗口已经覆盖 t 中全部字符尝试收缩 while formed required: if right - left 1 ans[0]: ans (right - left 1, left, right) # 从窗口移除最左边字符 left_char s[left] window[left_char] - 1 if left_char in need and window[left_char] need[left_char]: formed - 1 left 1 return s[ans[1]: ans[2] 1] if ans[0] ! float(inf) else 这里最关键的点在于formed只在“某个字符的计数从不足变成满足”时加一在“从满足变成不足”时减一。因为每类字符只会被封顶一次所以formed不会无限增长算法才能在 O(n) 内完成。我在面试中见过不少人用朴素方法每次检查窗口是否覆盖 t 时都重新扫描一遍 t然后整体复杂度就退化成了 O(n*|t|)这在大数据量下基本就是送命。用可变窗口时一定要祈祷一件事窗口状态的维护是单调且有界的。所谓“单调”是指formed这类指标只会因窗口伸缩而产生有限的增减所谓“有界”是指哈希表的大小不会无限制膨胀。只要这两点成立整个算法的时间复杂度就是 O(n) 级别这也是滑动窗口能作为“优选算法”存在的根基。4. 单调队列优化滑动窗口最大值的正解4.1 为什么普通队列和优先队列都不够好固定窗口里有一个非常经典的问题给定数组nums和窗口大小k求每个长度为 k 的窗口内的最大值。很多人一上来就用大顶堆优先队列每次窗口移动时把新元素入堆把出窗口的元素做标记然后取堆顶最大值。这个思路可行但有个隐患当你从堆顶弹出最大值之后如果这个最大值其实已经不属于当前窗口了你还得继续找到下一个仍在窗口内的最大值这就导致堆中会残留大量过期元素。每次取最大值的代价虽然是 O(log k)但堆里累计的过期元素多了以后内存和实际运行时间都会变差。有人可能会说那我直接用普通队列每次窗口滑动时遍历一遍窗口内元素找最大值不就没有过期问题了吗没错但每次找最大值要 O(k)整体复杂度会退化到 O(n*k)窗口稍微大一点就完全跑不动。真正高效且优雅的做法是单调队列。它本质上是一个双端队列但队列里的元素保持严格单调递减或递增的顺序。窗口每移动一次我们做两件事从队尾入队新元素入队前把所有比新元素小的队尾元素全部弹出。从队头弹出已经不在当前窗口内的元素。这样队头永远都是当前窗口的最大值。整个过程的均摊复杂度是 O(1)整体算法是 O(n)。4.2 单调队列的维护规则一步一步演示我拿一个具体例子走一遍。假设nums [1,3,-1,-3,5,3,6,7]k 3。我们用一个双端队列q存的是数组下标而不是元素值因为只有保存下标才能判断某个元素是否还在窗口内。初始 i0元素 1。队列为空直接把 0 入队。队列[0]对应值[1]。i1元素 3。队尾元素是nums[0]11 小于 3将 0 弹出然后 1 入队。队列[1]对应值[3]。此时 i1 还没到窗口长度 3不输出。i2元素 -1。队尾元素是nums[1]33 不小于 -1所以 -1 可以直接入队。队列[1,2]对应值[3,-1]。当前窗口[1,3,-1]最大值是 3输出nums[q[0]]3。i3元素 -3。队尾nums[2]-1-3 不比 -1 大直接入队。队列[1,2,3]对应值[3,-1,-3]。但注意队头下标 1 已经小于 i-k11i-k1 1所以队头还在窗口内。当前窗口[3,-1,-3]最大值 3输出 3。i4元素 5。队尾nums[3]-3弹出再往前nums[2]-1弹出再往前nums[1]33 小于 5也弹出。队列空了4 入队。队列[4]对应值[5]。当前窗口[-3,5,3]最大值 5输出 5。i5元素 3。队尾nums[4]55 不小于 33 入队。队列[4,5]对应值[5,3]。队头下标 4 仍在窗口内。当前窗口[5,3,6]等等这里要小心窗口是 i-k1 3 到 5也就是原数组下标 3、4、5 对应[-3,5,3]。但我上面写串了。你只需要知道结论输出 5。i6元素 6。队尾nums[5]3弹出队尾nums[4]5弹出6 入队。队列[6]输出 6。i7元素 7。队尾 6 弹出7 入队。队列[7]输出 7。最终输出的结果是[3,3,5,5,6,7]。对应每次窗口的最大值大家可以自己拿原数组验证一下完全正确。这里有一个关键操作顺序先清理队头过期元素再清理队尾的小元素最后入队新元素并输出当前窗口答案。顺序如果反了可能导致元素还没入队就去判断队头过期或者把新元素弹掉了逻辑就会出错。完整代码from collections import deque def max_sliding_window(nums, k): q deque() res [] for i, v in enumerate(nums): # 移除已经离开窗口的队头下标 if q and q[0] i - k: q.popleft() # 移除队尾所有小于当前值的元素维持单调递减 while q and nums[q[-1]] v: q.pop() q.append(i) # 窗口成型后开始记录 if i k - 1: res.append(nums[q[0]]) return res单调队列这道题的代码虽然短但它是“滑动窗口 数据结构”的组合题里最常考的一种。如果你能在白板上讲清楚“为什么队尾要弹出比它小的元素”基本上就能证明你真的理解了这层优化而不是背模板。4.3 单调队列之外的变式和延伸理解了单调队列求最大值求最小值就是把条件反过来维护一个单调递增队列队头永远是当前窗口的最小值。两者都理解了以后可以试着做一些组合题——比如“滑动窗口中位数”“滑动窗口众数”“滑动窗口内不同元素的个数”这些题会和堆、哈希表、平衡树等其他数据结构交叉但底层的窗口滑动思路始终不变。我也顺便提一句单调栈。单调栈处理的是“找左边/右边第一个比当前元素大/小的元素”这类问题它和单调队列的核心思想高度一致都是通过去掉无用的中间状态来维护某种单调性。区别在于一个作用于栈一个作用于双端队列。把这两个放在一起对比学习效果会好很多。5. 滑动窗口在真实工程里的样子5.1 网络传输与流量控制中的窗口思想滑动窗口这个词其实不只是在算法题里用它在计算机网络的传输控制里同样是个核心概念。传输层协议里经常提到的“窗口”表示发送方在不等待确认的情况下最多还能发送多少数据。窗口越大单位时间内能传的数据就越多但网络拥堵时窗口会被调小避免丢包和重传。虽然网络协议里的窗口尺寸调整逻辑比刷题复杂得多但它们的基本思路是完全一致的维护一个连续的范围按条件前后滑动以此控制一段数据的处理量。理解这一点对工程师来说挺重要因为很多底层网络和分布式系统里的“限流”策略也是这个思路。比如固定窗口计数在时间轴上划分出一段段等长的窗口每段窗口内允许一定数量的请求通过时间走到下一个窗口计数器清零重新计数。这种策略实现简单但缺点是可能存在窗口交接处的双倍流量峰值所以后来又有了滑动窗口日志、滑动窗口计数器等更平滑的方案。5.2 信号滤波与传感器数据处理滑窗均值另一个非常贴近日常的场景是信号滤波尤其是传感器数据的平滑处理。如果你做过姿态解算、温度采集、声音强度检测之类的项目一定遇到过“数据毛刺太大直接读数根本没法用”的问题。最简单的处理办法就是“滑动窗口均值滤波”维护最近 N 个采样值每次新数据到来时去掉最旧的一个、加入最新的一个然后求窗口内的平均值用这个平均值作为当前输出。对应热词里的“滑动窗口滤波模型”和“滑动窗口滤波器延迟”我再展开说一句。窗口越大平滑效果越好但延迟也越大——因为输出是对过去 N 个时刻的平均N 越大系统对信号的响应越迟钝。这个“延迟”是滑窗滤波天生的特性没有任何参数能完全消除。工程上一般会在“平滑度”和“响应速度”之间取一个平衡比如温度传感器用 5 到 10 个点的窗口姿态解算里的互补滤波或 Kalman 滤波本质上也借鉴了类似的“历史状态加权”思想。写一个非常简化的滑窗平均滤波示例from collections import deque class SlidingWindowAverage: def __init__(self, window_size): self.window_size window_size self.window deque(maxlenwindow_size) self.total 0 def update(self, value): if len(self.window) self.window_size: self.total - self.window[0] self.window.append(value) self.total value return self.total / len(self.window)这个类里面其实就蕴含了滑动窗口最核心的“加新减旧”逻辑和刷题时的窗口求和几乎一模一样。区别只在于数据来源是实时流而不是静态数组。5.3 日志聚合、实时统计与数据库分页滑动窗口在工程里还有一个常见形态是基于时间窗口的实时统计。比如“最近 5 分钟的错误日志数量”你自然不能每次都从全量日志里重新扫一遍而是维护一个 FIFO 队列新日志进来超过 5 分钟的老日志就移出去队列长度随时反映最近 5 分钟的总量。这也是滑动窗口在“流式数据”场景下的直接应用。包括数据库里的分页查询如果你把“LIMIT offset, size”看作一个固定窗口每次翻页就相当于窗口整体往后移动 size 行——虽然它的实现和内存中的滑动窗口不是一回事但那种“框住一段连续数据按顺序推进”的直觉是一脉相承的。这也是为什么我认为滑动窗口不仅是一种刷题技巧更是一种通用的“增量维护连续区间状态”的工程思维。在工程里用滑动窗口的时候我不太建议为了省内存把窗口压缩成几个聚合值。因为一旦后面有“窗口回溯”的需求比如要看某个时刻窗口内的完整数据聚合值就救不回来了。该维护队列就维护队列只有在能明确接受信息丢失的前提下才考虑只存统计量。6. 常见问题与排查心得实录6.1 死循环与超时while 条件的三种典型错误可变窗口最常见的问题就是死循环。我总结了三种几乎每个人都犯过的错误。第一种是收缩循环没有正确终止。比如在“无重复字符最长子串”里如果用if freq[ch] 1而不是while那一次只能移出一个字符当窗口里有两个重复字符时根本清不干净于是外层循环继续跑但状态一直不合法最终结果错误或者卡死。解决办法很简单凡是收缩操作先问自己一句“收缩一次够吗”不够就上 while。第二种是左右指针相遇后还在收缩。当left已经大于right时窗口已经没有意义了此时再去访问s[left]就会越界。保险做法是内层循环加left right的边界条件或者在收缩前判断left right。这看起来是个很小的问题但当window_size为 0 或者输入为空字符串的时候就是个大坑。第三种是状态更新顺序反了。比如先移动了left再减去s[left]结果减掉的不是打算移除的那个字符。我在实际调代码时发现最好的习惯是“先根据当前 left 位置更新状态再 left 1”顺序固定能少一大半低级错误。6.2 越界与结果遗漏边界值处理的三个经验边界问题在滑动窗口题里非常普遍尤其是那些从 1 开始计数的场景。我分享三个高频经验。第一循环里right从 0 到 n-1 跑完整个数组后最后一个窗口的状态往往没有被记录。比如固定窗口题里有人只在循环体内更新结果但因为初始化时没有预处理第一个窗口导致第一个窗口的结果被漏掉或者可变窗口里所有合法窗口也只会在 for 循环体内被记录如果最后一次right移动后窗口完全合法但循环刚好结束就可能漏答案。解决思路是固定窗口先预处理第一个窗口可变窗口在循环体内每步移动都尝试更新答案。第二数组为空、窗口长度比数组大、目标字符串为空这几种特殊输入在代码里最好用一两个 if 直接挡掉而不是塞进通用逻辑里去硬算。比如“最短覆盖子串”里如果t为空字符串答案应该直接返回空串根本不用进循环。第三哈希表里的频次可以出现负数吗在“最小覆盖子串”这类题中左边界收缩时会不断减少字符频次某个字符可能在 t 中出现过但已经被窗口完全移出此时频次变成 0 甚至负数。这不一定是坏事但如果你用“频次是否大于 0”来判断字符是否在窗口里就会出错。建议统一约定频次只表示“窗口与目标的差值”不做额外语义判断。6.3 如何一眼识别滑动窗口题五个判断信号最后我把“什么题适合用滑动窗口”总结成几个判断信号这些经验主要来自 LeetCode 刷题和面试复盘对我个人来说准确率很高。第一题目描述里出现“连续子数组”“连续子串”字样并且要求在连续区间上找最大值、最小值、长度、计数。第二数据规模在10^5左右暴力的 O(n^2) 必然超时而 O(n) 的滑动窗口正好合适。第三要求的最优解与某个“可变区间长度”相关而不是固定在某一个坐标点上。第四窗口内状态能通过增删元素快速更新。第五往往会有两个指针同时移动的直觉也就是常说的“双指针”。当然也有不适合滑动窗口的情况。比如子序列问题不要求连续滑动窗口就无法直接派上用场得改用动态规划再比如窗口内的状态没法增量维护必须重新全量计算那滑动窗口的优势就不存在了。遇到这些题硬套模板只会越写越乱。我个人做这类题的经验是先别急着写代码拿一支笔在纸上把left和right的移动轨迹画出来标出每一轮窗口的合法/不合法状态再写代码就是水到渠成的事。很多人写不出来不是因为不会语法而是因为脑子里根本没有“某一轮窗口长什么样”的画面。滑动窗口作为一类经典的优选算法它真正教给我们的不是一个模板而是一种思维方式面对大量连续区间的计算如何通过“加新减旧”的方式避免重复劳动。这层思考无论在刷题、面试还是工程实现里都极其受用。希望这篇从原理到实践、再到避坑心得的拆解能帮你真正拿下这个看似简单、实则暗藏玄机的算法。
返回列表