239. 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
在线性时间复杂度内解决此题?
思路-单调队列
参考:https://www.programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html#%E6%80%9D%E8%B7%AF
我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。
这个队列应该长这个样子:
from collections import deque
class MyQueue:
def __init__(self):
self.queue = deque()
def pop(self, value):
pass
def push(self, value):
pass
def front(self, value):
pass
每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。
然后再分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。
但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。
那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。
大家此时应该陷入深思…
其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
那么这个维护元素单调递减的队列就叫做单调队列,即单调递减或单调递增的队列。目前没有直接支持单调队列,需要我们自己来实现一个单调队列
不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。
来看一下单调队列如何维护队列里的元素。
动画如下:
对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。
此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口进行滑动呢?
设计单调队列的时候,pop,和push操作要保持如下规则:
pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止
保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。
为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下:
那么我们用什么数据结构来实现这个单调队列呢?
使用deque最为合适(python中也有)
deque用法参考:https://blog.csdn.net/weixin_45666566/article/details/1084409
append、pop、extend默认是右端操作,而appendleft、popleft、extendleft是左端操作。
代码
from collections import deque # 在python中也有双向队列的数据结构
# [0] 和 [-1] 是相对的
class MyQueue: # 单调队列
def __init__(self):
self.queue = deque() # 这里需要使用deque实现单调队列 直接使用list会超时
# 每次pop的时候 要判断队列不为空才可以pop 另外只有当出口处的元素等于当前value的时候 才pop
def pop(self, value):
if self.queue and self.queue[0] == value:
self.queue.popleft() # list.pop()时间复杂度为O(n),这里需要使用collections.deque()
# 在push的时候 如果现在进来的元素要大于队列内的元素 则队列后端的元素就要pop出去 直到现在进来的元素小于队列出口处的元素
#这样就保持了队列里的数值是单调从大到小的了
def push(self, value):
# push前先清理一下队列内的元素
# 为什么是-1呢 可以画个草图分析 见图1
while self.queue and value > self.queue[-1]: # deque不为空 且现在进来的元素大于队列出口元素
self.queue.pop() # 那么此时出口的元素就要pop掉
self.queue.append(value)
#查询当前队列里的最大值 直接返回队列前端([0])也就是front就可以了。
def front(self):
return self.queue[0]
class Solution(object):
def maxSlidingWindow(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
que = MyQueue()
result = [] # 存放结果
# 前k个元素先入队列
for i in range(k):
que.push(nums[i])
result.append(que.front()) # 前k个元素中的最大值
# 后面的元素
for i in range(k, len(nums)):
que.pop(nums[i-k]) # 每个窗口中最前面的元素要出去
que.push(nums[i]) # 后面的元素 入队列
result.append(que.front()) # 将每个窗口中的最大值加入结果
return result
- 时间复杂度: O(n)
- 空间复杂度: O(k)
再来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。
有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。
其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。
空间复杂度因为我们定义一个辅助队列,所以是O(k)。
扩展
需要要明确的是,题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本题中的单调队列实现就是固定的写法哈。
参考:https://www.programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html