• 239. 滑动窗口最大值(Hard)-双端队列之单调队列


    239. 滑动窗口最大值

    给你一个整数数组 nums,有一个大小为 k 滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

    返回 滑动窗口中的最大值 

    示例 1:

    1. 输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
    2. 输出:[3,3,5,5,6,7]
    3. 解释:
    4. 滑动窗口的位置 最大值
    5. --------------- -----
    6. [1 3 -1] -3 5 3 6 7 3
    7. 1 [3 -1 -3] 5 3 6 7 3
    8. 1 3 [-1 -3 5] 3 6 7 5
    9. 1 3 -1 [-3 5 3] 6 7 5
    10. 1 3 -1 -3 [5 3 6] 7 6
    11. 1 3 -1 -3 5 [3 6 7] 7

    示例 2:

    1. 输入:nums = [1], k = 1
    2. 输出:[1]

    提示:

    • 1 <= nums.length <= 105
    • -104 <= nums[i] <= 104
    • 1 <= k <= nums.length

    题目解析

    滑动窗口最大值,是用队列解决的经典问题,难度困难。

    单调队列

    在开始之前我先介绍一个之前没有讲过的队列,叫双端队列

    普通队列是限制仅在队尾进行插入,在队头进行删除操作的线性表,队列的插入叫做入队列,队列的删除叫做出队列。

    而双端队列则是放开了这个限制,在队头和队尾两端都可以进行入队和出队操作的队列。

    这么细看,其实对于队头或者队尾端,相当于是一个栈,后进的先出。

    双端队列看上去这么的像栈和队列的结合体。

    而有些时候,双端队列中还有受限的双端队列:一个是输出受限的双端队列,另一个是输入受限的双端队列。

    输出受限的双端队列是:允许在一端进行入队和出队,但在另一端只允许入队的双端队列。

    输入受限的双端队列是:允许在一段进行入队和出队,但在另一端只允许出队的双端队列。

    输出受限的双端队列里,有一种情况,那就是队列里的各元素之间的关系具有单调性,这叫单调队列。

    单调队列,顾名思义,所有队列里的元素都是按递增(递减)的顺序队列,这个队列的头是最小(最大)的元素。

    题解

    首先窗口向右滑动的过程就是将窗口最左侧的元素删除,同时在窗口的最右侧添加一个新的元素,这就要用到双端队列,然后找双端队列中的最大元素。

    那剩下就是如何找到滑动窗口中的最大值。

    那我们就可以只在队列中保留可能成为窗口最大元素的元素,去掉不可能成为窗口中最大元素的元素。

    想象一下,如果要进来的是个值大的元素,那一定会比之前早进去的值小的元素晚离开队列,而且值大的元素在,都没值小的元素啥事,所以值小的元素直接弹出队列即可

    这样队列里其实维护的一个单调递减的单调队列。

    图解

    注:因为代码模拟方便,单调队列里存的是数组下标,在这为了演示的更加直观,图解中单调队列中用的是元素值。

    假设 nums = [1, 3, -1, -3, 5, 3, 6, 7],k = 3。

    首先初始化一个单调队列和结果数组。

    1. # 如果数组为空或 k = 0,直接返回空
    2. if not nums or not k:
    3. return []
    4. # 如果数组只有1个元素,直接返回该元素
    5. if len(nums) == 1:
    6. return [nums[0]]
    7. # 初始化队列和结果,队列存储数组的下标
    8. queue = []
    9. res = []

     第 1 步,数值为 1,此时单调队列为空,直接入队列。

    第 2 步,数值为 3,queue 中有一个元素 1,3 > 1,所以按照我们之前说的,3 如果进入队列,那最大值只能会是 3,没有 1 的出头之日,所以 1 直接出队列即可。

    1. # 对于新进入的元素,如果队列前面的数比它小,那么前面的都出队列
    2. while queue and nums[queue[-1]] < nums[i]:
    3. queue.pop()
    4. # 新元素入队列
    5. queue.append(i)

    第 3 步,数值为 -1,-1 < 3,所以 -1 直接入队列。

    此时走过了 k = 3 个元素,当前的最大值就是单调队列最左侧的值,也就是 3。

    1. # 当前的大值加入到结果数组中
    2. if i >= k-1:
    3. res.append(nums[queue[0]])

    第 4 步,窗口右移,此时对应下标数值为 -3,-3 < -1,所以 -3 直接入队列。

    此时最大值依然还是 3。

    第 5 步,窗口右移,下标 1 对应的数值 3 不在窗口内,但是 3 在 queue 内,所以 3 出队列。

    1. # 如果当前队列最左侧存储的下标等于 i-k 的值,代表目前队列已满。
    2. # 但是新元素需要进来,所以列表最左侧的下标出队列
    3. if queue and queue[0] == i - k:
    4. queue.pop(0)

     此时下标对应的数值为 5,5 分别大于 -1 和 -3,所以 -1 和 -3 出队列,5 入队列,此时的最大值为 5。

    第 6 步,窗口右移,当前下标对应的数值为 3,3 < 5,所以 3 直接入队列,此时的最大值是 5。

    第 7 步,窗口右移,当前下标对应的数值为 6,6 大于 queue 中的 5 和 3,所以 queue 中的 5 和 3 出队列,然后 6 入队列,此时最大的值为 6。

    第 8 步,窗口右移,当前下标对应的数值为 7,7 > 6,所以 queue 中 6 出队列,7 入队列,当前最大值为 7。

    此时数组扫描完,直接输出 res 的结果即可。

    本题的解法,数组被从头到尾扫描一遍,每个元素的下标最多进出队列一次,所以时间复杂度为 O(n)。

    因为创建了一个额外的大小为 k 的队列存储,所以本题空间复杂度为 O(k)。

    代码实现:

    1. class Solution:
    2. def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
    3. # while(right < len(nums)):
    4. # res.append(max(nums[left:left+k]))
    5. # left += 1
    6. # # window.append(nums[right])
    7. # right += 1
    8. # 如果数组为空或 k = 0,直接返回空
    9. if not nums or not k:
    10. return []
    11. # 如果数组只有1个元素,直接返回该元素
    12. if len(nums) == 1:
    13. return [nums[0]]
    14. window, res = [], []# window存储数组的下标,res存储最大值结果
    15. for i in range(len(nums)):
    16. # 如果当前队列最左侧存储的下标等于 i-k 的值,代表目前队列已满。
    17. # 但是新元素需要进来,所以列表最左侧的下标出队列
    18. if(window and window[0] == i-k):
    19. window.pop(0)
    20. # 对于新进入的元素,如果队列前面的数比它小,那么前面的都出队列(单调递减队列实现)
    21. while(window and nums[window[-1]] < nums[i]):#window[-1]表示队尾的最后一个元素
    22. window.pop()#队尾最后一个元素出队(因为要保证队首元素为最大值,所以只能从右边出队)
    23. # 新元素下标入队列,窗口右移动
    24. window.append(i)
    25. # 当前的大值加入到结果数组中
    26. if(i >= k-1):
    27. res.append(nums[window[0]])
    28. return res

     参考:ACM 选手图解 LeetCode 滑动窗口最大值 | 编程文青李狗蛋

  • 相关阅读:
    15 Transformer 框架概述
    SpringBoot整合MyBatis-Plus
    👍SpringSecurity单体项目最佳实践
    在移动硬盘上安装Win11系统(不使用工具)
    [如何在VS code中使用mysql](使用sqltools插件)
    u盘上面 安装 ubuntu 系统
    FFmpeg之转码
    leetcode_27_最小栈
    Jenkins集成newman
    chatGPT马上要支持应用商痁了 个人开发GPT应用赚钱的时代来了 海外淘金GPT4 Turbo版
  • 原文地址:https://blog.csdn.net/wangjian530/article/details/127514745