一、题目描述

给定一个数组 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

提示:

1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length

二、解题思路 & 代码

2.1 暴力法

最简单直接的方法是遍历每个滑动窗口,找到每个窗口的最大值。一共有 ​​N - k + 1​​​ 个滑动窗口,每个有 ​​k​​​ 个元素,于是算法的时间复杂度为 【LeetCode】239. 滑动窗口最大值_leetcode,表现较差。

class Solution:
def maxSlidingWindow(self, nums: 'List[int]', k: 'int') -> 'List[int]':
n = len(nums)
if n * k == 0:
return []

return [max(nums[i:i + k]) for i in range(n - k + 1)]

复杂度分析

  • 时间复杂度:【LeetCode】239. 滑动窗口最大值_队列_02。其中 N 为数组中元素个数。
  • 空间复杂度:【LeetCode】239. 滑动窗口最大值_数据结构_03,用于输出数组。

2.2 双向队列

遍历数组,将 数 存放在双向队列中,并用 ​​L​​​,​​R​​​ 来标记窗口的左边界和右边界。队列中保存的并不是真的 数,而是该数值对应的数组下标位置,并且数组中的数要从大到小排序。如果当前遍历的数比队尾的值大,则需要弹出队尾值,直到队列重新满足从大到小的要求。刚开始遍历时,​​L​​​ 和 ​​R​​​ 都为 0,有一个形成窗口的过程,此过程没有最大值,​​L​​​ 不动,​​R​​​ 向右移。当窗口大小形成时,​​L​​​ 和 ​​R​​​ 一起向右移,每次移动时,判断队首的值的数组下标是否在 ​​[L,R]​​ 中,如果不在则需要弹出队首的值,当前窗口的最大值即为队首的数。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]

解释过程中队列中都是具体的值,方便理解,具体见代码。
初始状态:L=R=0,队列:{}
i=0,nums[0]=1。队列为空,直接加入。队列:{1}
i=1,nums[1]=3。队尾值为1,3>1,弹出队尾值,加入3。队列:{3}
i=2,nums[2]=-1。队尾值为3,-1<3,直接加入。队列:{3,-1}。此时窗口已经形成,L=0,R=2,result=[3]
i=3,nums[3]=-3。队尾值为-1,-3<-1,直接加入。队列:{3,-1,-3}。队首3对应的下标为1,L=1,R=3,有效。result=[3,3]
i=4,nums[4]=5。队尾值为-3,5>-3,依次弹出后加入。队列:{5}。此时L=2,R=4,有效。result=[3,3,5]
i=5,nums[5]=3。队尾值为5,3<5,直接加入。队列:{5,3}。此时L=3,R=5,有效。result=[3,3,5,5]
i=6,nums[6]=6。队尾值为3,6>3,依次弹出后加入。队列:{6}。此时L=4,R=6,有效。result=[3,3,5,5,6]
i=7,nums[7]=7。队尾值为6,7>6,弹出队尾值后加入。队列:{7}。此时L=5,R=7,有效。result=[3,3,5,5,6,7]
  • 通过示例发现​​R=i​​​,​​L=k-R​​。由于队列中的值是从大到小排序的,所以每次窗口变动时,只需要判断队首的值是否还在窗口中就行了。
  • 解释一下为什么队列中要存放数组下标的值而不是直接存储数值,因为要判断队首的值是否在窗口范围内,由数组下标取值很方便,而由值取数组下标不是很方便。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if len(nums) < 2:
return nums
queue = collections.deque()
res = [0] * (len(nums)-k+1)
for i in range(len(nums)):
#保证从大到小 如果前面数小则需要依次弹出,直至满足要求
while queue and nums[queue[-1]] <= nums[i]:
queue.pop()
queue.append(i)
#判断队首值是否有效
if queue[0] <= i - k:
queue.popleft()
#当窗口长度为k时 保存当前窗口中最大值
if i + 1 >= k:
res[i+1-k] = nums[queue[0]]
return

复杂度分析

  • 时间复杂度:【LeetCode】239. 滑动窗口最大值_python_04,每个元素被处理两次- 其索引被添加到双向队列中和被双向队列删除。
  • 空间复杂度:【LeetCode】239. 滑动窗口最大值_python_04,输出数组使用了【LeetCode】239. 滑动窗口最大值_python_06空间,双向队列使用了【LeetCode】239. 滑动窗口最大值_python_07

参考:

  1. ​LeetCode优秀题解评论区​

简化版:

class Solution(object):
def maxSlidingWindow(self, nums, k):
win, ret = [], []
for i, v in enumerate(nums):
if i >= k and win[0] <= i - k:
win.pop(0)
while win and nums[win[-1]] <= v:
win.pop()
win.append(i)
if i >= k - 1:
ret.append(nums[win[0]])
return

参考:

  1. ​LeetCode评论区​