单调队列——本质上和单调栈是一样的思路
算法学习笔记(66): 单调队列
“如果一个选手比你小还比你强,你就可以退役了。”——单调队列的原理
好久没写笔记了,先补一个简单的。单调队列是一种主要用于解决滑动窗口类问题的数据结构,即,在长度为 n 的序列中,求每个长度为 m 的区间的区间最值。它的时间复杂度是 O(n) ,在这个问题中比 O(nlogn) 的ST表和线段树要优。

单调队列的基本思想是,维护一个双向队列(deque),遍历序列,仅当一个元素可能成为某个区间最值时才保留它。
形象地打个比方,上面的序列可以看成学校里各个年级XCPC选手,数字越大代表能力越强。每个选手只能在大学四年间参赛,毕业了就没有机会了。那么,每一年的王牌选手都在哪个年级呢?
一开始的时候,大三大四的学长都比较菜,大二的最强,而大一的等大二的毕业后还有机会上位,所以队列里有两个数。

一年过去了,原本大一的成为大二,却发现新进校的新生非常强,自己再也没有机会成为最大值了,所以弹出队列。

又过了一年,新入校的新生尽管能力只有1,但理论上只要后面的人比他还菜,还是可能成为区间最大值的,所以入队。

终于,原本的王牌毕业了,后面的人以为熬出头了,谁知道这时一个巨佬级别的新生进入了集训队,这下其他所有人都没机会了。

(这只是比方,现实中各位选手的实力是会增长的,不符合这个模型ovo)
总之,观察就会发现,我们维护的这个队列总是单调递减的。如果维护区间最小值,那么维护的队列就是单调递增的。这就是为什么叫单调队列。
代码也很简洁:
deque<int> Q; // 存储的是编号
for (int i = 0; i < n; ++i)
{
if (!Q.empty() && i - Q.front() >= m) // 毕业
Q.pop_front();
while (!Q.empty() && V[Q.back()] < V[i]) // 比新生弱的当场退役(求区间最小值把这里改成>即可)
Q.pop_back();
Q.push_back(i); // 新生入队
if (i >= m - 1)
cout << V[Q.front()] << " ";
}
我自己python写的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | from collections import deque def slide_window(queue, arr, window): ans = [queue[ 0 ]] for i in range (window, len (arr)): while queue and queue[ - 1 ] < arr[i]: queue.pop() queue.append(arr[i]) if arr[i - window] = = queue[ 0 ]: queue.popleft() ans.append(queue[ 0 ]) return ans def init_queue(arr, window): # [1, 3, 6, 2] ==> queue: [6, 2] queue = deque() for i in range ( 0 , window): while queue and queue[ - 1 ] < arr[i]: queue.pop() queue.append(arr[i]) return queue def find_max_window(arr, window): # initialize queue queue = init_queue(arr, window) # slide window return slide_window(queue, arr, window) if __name__ = = '__main__' : arr = [ 1 , 3 , 6 , 2 , 5 , 1 , 7 ] print (find_max_window(arr, window = 4 )) # [6, 6, 6, 7] |
重构下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from collections import deque def find_max_window(arr, window): ans = [] queue = deque() # 如果是单调栈,则修改为 stack = [],后面的queue修改为stack for i in range ( 0 , len (arr)): # 单调栈固定模板,就是一个while循环 while queue and queue[ - 1 ] < arr[i]: queue.pop() # 满足条件后直接append元素 queue.append(arr[i]) # 如果是单调队列,则需要处理队头元素,如下是满足窗口过期元素,则popleft删除队头元素 if arr[i - window] = = queue[ 0 ]: queue.popleft() # 处理结果变量,根据不同的场景替换 if i > = window - 1 : ans.append(queue[ 0 ]) return ans if __name__ = = '__main__' : arr = [ 1 , 3 , 6 , 2 , 5 , 1 , 7 ] print (find_max_window(arr, window = 4 )) # [6, 6, 6, 7] |
239. 滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from collections import deque class Solution: def maxSlidingWindow( self , nums: List [ int ], k: int ) - > List [ int ]: q = deque() for i in range (k): while q and nums[i] > q[ - 1 ]: q.pop() q.append(nums[i]) ans = [q[ 0 ]] for i in range (k, len (nums)): while q and nums[i] > q[ - 1 ]: q.pop() q.append(nums[i]) if nums[i - k] = = q[ 0 ]: q.popleft() ans.append(q[ 0 ]) return ans |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2016-12-16 JavaScript EventLoop
2016-12-16 node 事件循环