简介
题目链接 LeetCode 239. Sliding Window Maximum
本题的标签是Heap
,Sliding Window
和Dequeue
,是滑动窗口和双端队列结合的一道经典题。本文将着重讲解双端队列解法,同时对动态规划的解法进行简单的讲解。
注意:文章中一切注解皆为Python代码
理解题目
题目非常简单。给定一个数列,和一个窗口,在窗口从左向右滑动的时候,持续计算窗口中子数列的最大值。
最暴力的解法就是在窗口滑动的过程中持续计算最大值,代码就一行,如下
class Solution:
def maxSlidingWindow(self, nums: 'List[int]', k: 'int') -> 'List[int]':
return [max(nums[i-k:i]) for i in range(k, len(nums)+1)]
很明显,暴力解法需要的时间复杂度是O(NK)
,其中N
是数据规模,K
是窗口大小。如果窗口大小K
等于N/2
的话,时间复杂度可以表示为O(N^2)
。对于题目中输入范围为105 这种级别,显然O(N^2)
这种时间复杂度是不行的,因此需要探索其他解法。
题目剖析+思考过程
想到更优的解法其实并不难,首先我们要意识到这题的两个操作,一是遇到新数字时对当前最大值的更新,二是淘汰过期数字时对当前最大值的更新。
对于第一种情况,我们都知道一个前缀数组(prefix array
)就可以解决。那么如何去淘汰过期的数字,并更新最大值呢?显然单纯的前缀数组不好使了,怎么办:
- 首先,提到“过期”可能最先想到的就是队列(
queue
),先进先出 - 其次,淘汰“过期”数字后,需要选出第二大的数字,依次类推,可能还需要第三、第四、第K大的数字
那什么样的数据结构可以高效的提供这样(内容不断变化且能保持一定顺序)的信息呢?
- 堆(
heap
)就可以 - 单调递减的双端队列也可以
堆
这里的堆(heap
)很好理解,堆的加入和删除都仅仅耗费O(logK)
,并且,它时时刻刻能保证顺序;最终的时间复杂度可以近似为O(NlogN)
。
那么如何去理解单调递减的双端队列呢?
单调递减双端队列
双端队列本身不用多讲,先进先出,两端都可以以O(1)
的时间加入和删除;对于单调递减,我们事实上是为了维护一种单调递减的顺序,也就是之前我提到的
淘汰“过期”数字后,需要选出第二大的数字,依次类推,可能还需要第三、第四、第K大的数字
这本质上是一种单调递减的状态,看个例子:
给出数列[9,5,7,6,11]
,窗口长度为3
,起初单调递减队列dq
为空,指针i = 0
i = 0, dq = [9], 当前max = 9, 窗口中数列为[9], 滑动最大值还未开始计算,因为窗口内数组不满3个
i = 1, dq = [9, 5], 当前max = 9, 窗口中数列为[9, 5], 滑动最大值还未开始计算,因为窗口内数组不满3个
i = 2, dq = [9, 7], 当前max = 9, 窗口中数列为[9, 5, 7], 滑动最大值[9]
;- 注意为了保证队列的单调递减性,之前的5被弹出来,7加了进去
- 因为只要7还有没有过期,7肯定比5大
- 而且7在5之后,肯定比5要更晚过期
i = 3, dq = [7,