leetcode 239 滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例:
输入: 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
解题思路:
-
解法一【O(N·logK)】
通常做最大值和最小值这种题目,我觉得使用优先队列(堆)是比较方便的,这里我们要求最大值,那么就维护一个size为K的堆,然后每次往后滑动,就扔掉第一个数,放入新元素,再次调整堆,调整堆的复杂度为O(logK),总共的时间复杂度为O(N·logK) -
解法二【O(N)】
使用双端队列deque,双端队列的特点是可以从两端都入队和出队. 核 心 思 想 是 最 大 值 永 远 是 队 列 中 最 左 边 的 值 \color{red}{核心思想是最大值永远是队列中最左边的值} 核心思想是最大值永远是队列中最左边的值, 依次放入数据,这里我把先进来的值叫做A,后进来的值叫做B,如果B小于A,那么就放在A后面,如果B大于A,那么就把A出队,B就是队列的最左边的值,以此类推,每次返回最大值。这里做的优化是不需要去维护堆里的k个元素的大小顺序,只需要记录最大值即可,所以用heap就有点杀鸡用牛刀了。
代码思路:
- 表示代码严谨性,判断输入是否为空
- 两个数组,window存放下标,res存放结果
- window[0] <= i-k 是为了判断是否超过左边界,如果超出,就pop出来
- 逻辑重点,nums[window[-1]] <= x,意思是若输入的数比window里的元素大,就把window里的这些比它小的元素出队. 这里就像[3,1,2],2比1大,所以会把1从右边踢出去,放入2,最大值仍然为3,往后走一步,3会被踢出去,此时2就成最左边的值了,这里突出的思路是每个元素只需要进入window一次,1进去一次就被踢出来了,不需要再重复判断,所以复杂度为O(1),
代码:
解法二:
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums or not k : return []
window,res = [],[]
for i,x in enumerate(nums):
if i>=k and window[0]<=i-k: #如果window已经走过k个数字,并且超出了左边界
window.pop(0) #踢出去最左边的元素
while window and nums[window[-1]] <= x:
window.pop()
#如果window不为空,且新进来的元素,比你前面的元素大,
#那么把前面元素给踢掉,放入新元素
window.append(i) #无论怎样都是要先进window一次的
if i>=k-1: #window走过了k个数字再开始记录res
res.append(nums[window[0]])
return res
复 杂 度 分 析 : \color{red}{复杂度分析:} 复杂度分析:
空间复杂度O(N),输出数组使用了
O
(
N
−
k
+
1
)
{O}(N - k + 1)
O(N−k+1)空间,双向队列使用了
O
(
k
)
{O}(k)
O(k)。
时间复杂度O(N)每个元素被处理两次:其索引被添加到双向队列中和被双向队列删除。
进阶分析:
打算实现一下解题思路:
第一个想到的是暴力法,遍历每个滑动窗口,找到每个窗口最大的值,一共N-k+1个滑动窗口,每个有K个元素,时间复杂度为
O
(
N
⋅
K
)
O(N·K)
O(N⋅K)
class Solution:
def maxSlidingWindow(self, nums: 'List[int]', k: 'int') -> 'List[int]':
n = len(nums)
if n*k == 0: #判断nums和k是否为空
return []
res = []
for i in range(n-k+1):
res.append(max(nums[i:i+k]))
return res