单调队列模板题-滑动窗口最大值

力扣题目链接

此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。

第一层:暴力法为什么不行?(你的想法)

你说的“直接比较每k个数字里最大的”叫暴力滑动
假设数组长度n=100000,窗口大小k=50000
每滑动一次,你就要比较 50000 次,一共要滑动 50001 次。总计算量是 25亿次,绝对超时。

第二层:单调队列到底在干嘛?(核心思想)

单调队列不是在“找最大值”,而是在维护一个“最大值候选人”的名单,并且按出场顺序排好队

关键洞察:当窗口滑动时,一个新元素进来了。如果这个新元素队列里末尾的某些旧元素还大,那么那些旧元素就永远不可能成为最大值了(因为它们比新元素小,而且还比新元素先过期)。所以,直接把那些没用的旧元素从队尾踢出去

这就是“单调递减队列”名字的由来——队列里从头到尾,数值越来越小。

第三层:为什么不需要精确弹出“被移除的那个元素”?(解决你的最大疑问)

你问:“已经排序之后的队列,怎么能把窗口要移除的元素弹出呢?”

答案是:我们根本不关心它是不是在队列里,我们只在它“恰好是队首”时才弹出它。

请看下面这个活生生的例子,你就全明白了:
数组[5, 3, 4, 1],窗口k=3

  1. 窗口 [5, 3, 4]

    • 5 进队:[5]

    • 3 进队:[5, 3](比5小,留着备用)

    • 4 进队:发现队尾的34小,把3踢掉(因为3没4大,还比4先过期,永无出头之日)。队里变成[5, 4]

    • 最大值= 队首5

  2. 窗口滑动,移除 5,加入 1

    • 关键操作:要移除的正是5

    • 此时一看,队首正好是5把它弹出!队列变成[4]

    • 加入 1,比4小,留在队尾:[4, 1]

    • 最大值= 队首4完美!


如果移除的元素不在队首呢?比如窗口里是[4, 1, 2],队里是[4, 2](1早就被2踢掉了)。当窗口移除4时,队首弹出4,剩下[2]。当窗口要移除1时,因为 1 早就不在队列里了,所以队列什么都不用做。因为 1 根本不可能是最大值,我们根本不需要关心它何时被移除。

这个操作让每个元素最多进出队列一次,总时间直接从O(n*k)降为O(n)

例如 数组:[1, 3, -1, -3, 5, 3, 6, 7],窗口大小k = 3
队列里存的是下标(方便判断过期),为了你看得懂,我写成值(下标)

步骤当前元素 (i)1. 清理过期(队首下标 < i-2 就弹出)2. 清理弱者(队尾值 < 新值就弹出)3. 入队后队列状态4. 当前窗口最大值(队首值)
11 (i=0)窗口未满,无需过期队空,无人可弹[ 1(0) ]窗口未满,不输出
23 (i=1)窗口未满,无需过期队尾 1 < 3,弹出1[ 3(1) ]窗口未满,不输出
3-1 (i=2)窗口满了,但队首下标1 >= 0,未过期队尾 3 < -1?否(3大于-1)[ 3(1), -1(2) ]输出 3
4-3 (i=3)窗口范围 [1,3],队首1未过期队尾 -1 < -3?否[ 3(1), -1(2), -3(3) ]输出 3
55 (i=4)窗口范围 [2,4],队首下标1 < 2过期!弹出3队尾 -3<5弹;-1<5弹;3<5弹[ 5(4) ]输出 5
63 (i=5)窗口范围 [3,5],队首4未过期队尾 5 < 3?否[ 5(4), 3(5) ]输出 5
76 (i=6)窗口范围 [4,6],队首4未过期队尾 3<6弹;5<6弹[ 6(6) ]输出 6
87 (i=7)窗口范围 [5,7],队首6未过期队尾 6<7弹[ 7(7) ]输出 7

一、我们到底在维护什么?(先看数据结构)

我们用一个双端队列(deque),里面只存数组下标(不存数值)。

队列的绝对规则(铁律):
队首队尾,这些下标对应的数值必须严格递减
(例如:队首是10,队尾是3,中间是7,即 10 > 7 > 3)

二、每次窗口移动,只做这三步(死记硬背)

假设当前遍历到下标i,窗口范围是[i - k + 1, i]

第1步(清理过期):
如果队首的下标小于i - k + 1(即这个下标已经不在当前窗口里了),就把队首从左边弹出去

第2步(清理弱者):
只要队尾对应的数值小于等于nums[i](新来的数),就把队尾从右边弹出去。(因为新来的数比你大,还比你晚过期,你永远没机会当最大值了)例如1 3 [-1 -3 5] 3 6 7

第3步(新王入队):
把当前下标i队尾放进去。

做完这三步,队首下标对应的数值,就是当前窗口的最大值。

代码还没写。