LeetCode hot 100 — 239. 滑动窗口最大值

题目信息

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

返回 滑动窗口中的最大值

示例 1:

复制代码
输入: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

示例 2:

复制代码
输入:nums = [1], k = 1
输出:[1]

提示:

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

初步的自己的思考

读完了这个题目,好像思维难度不大?没有觉得需要去想这个题目 要如何去处理?可能是压在了数据结构和时间复杂度上面?

写代码发现了一个问题,一个最大的难点是,如果此时滑动窗口的最大值刚好在最左侧,而新进来的数字要小于这个此时的最大值,那我就需要额外遍历去找到第二大的值,好像复杂化了这个问题

GPT启发思考

你要维护什么?你要用怎样的数据结构来维护这个东西?


换一个思路,当新进来一个数字 x

当前维护的数字中,比x小的数字还有意义吗? 答案是没有,因为我看的是最大值,而且数字x肯定比这个数字后离开滑动窗口。所以所有位于x左侧且不大于x的数字,都应该不再维护,被淘汰掉

例如当前候选者是:

复制代码
9, 7, 5

新数字 6 进入:

复制代码
5` 可以淘汰,因为 `6` 更大且离开得更晚

剩下:

复制代码
9, 7, 6

9 从窗口左侧离开时,7 就能立即接任最大值,不需要重新遍历窗口。

你接下来只需思考三个问题:

  1. 候选者的数值应该保持什么顺序,才能让最大值始终位于固定位置?
  2. 新数字进入时,需要从候选序列的哪一端淘汰较小数字?
  3. 窗口左侧数字过期时,如何判断候选者是否已经离开窗口?

第三个问题提示你:候选序列中最好保存数组下标,而不仅仅是数值。

最终你需要寻找一种结构,它能够:

复制代码
从一端删除已经过期的最大值
从另一端删除已经失去竞争资格的较小值
快速查看当前最大值

用示例手动模拟一下:

复制代码
nums = [1, 3, -1, -3, 5]
k = 3

候选变化如下:

复制代码
1 进入:      [1]
3 进入:      1 永远竞争不过更新的 3,所以淘汰 1
              [3]

-1 进入:     [3, -1]

第一个完整窗口是:

复制代码
[1, 3, -1]

候选队列是:

复制代码
[3, -1]

队头 3 就是最大值。

继续:

复制代码
-3 进入:     [3, -1, -3]

这时原来的 1 虽然离开窗口,但它早已不在候选队列中,所以不用处理它。

再向后滑动时,3 会离开窗口。因为 3 位于候选队头,可以根据它的下标判断它已经过期,然后删除,后面的候选者自然接班。

新数字 5 进入时:

复制代码
[3, -1, -3]

这些数字都不可能再竞争过更新的 5,所以可以从队尾连续淘汰,最后只剩:

复制代码
[5]

你现在可以把每一轮概括成三个动作:

  1. 删除队头已经离开窗口的下标。
  2. 从队尾淘汰所有不大于新元素的候选者。
  3. 把新元素的下标加入队尾。

参考代码

python 复制代码
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        """
        刷到现在,我感觉确实就是两个核心问题:
            - 你要维护什么?进阶的就是对于你维护的这个东西,你如何去更新(代码的核心逻辑
            - 你要用什么样的数据结构去维护这组量?
            
        自己初步思考以后,结合GPT的启发,给予以下的理解和思考:
            - 你维护的量不能只是当前 / 上一个 滑动窗口的最大值,这样维护的信息太片面了,当这个最大值离开当前滑动窗口,那么新的最大值会是新进入滑动窗口的值,还是还在滑动窗口的剩余值,又需要回到遍历,这样就会有逻辑冗余
            - 因此我需要维护的是所有的候选值,所有可能为 滑动窗口最大值 的那些候选值
            - 更新逻辑是什么?
                1. 首先是下标排除。不在滑动窗口的必须要排除
                2. 当扫描到一个新的数字x,所有候选窗口(已经扫过的数字中 的可能会出现的最大值) / 维护变量中,比这个数字小的都应该被删除。因为那些数字为较小值,且会更快的离开滑动窗口,所以当x出现后的所有合法滑动窗口中,之可能是x或者比x更大的数字
        """
        queue = deque()  # 用一个双端队列来维护候选值的下标!!(注意这里是记录下标而不是记录具体的值. 下标关联的东西更多, 可以看到是否在滑动窗口等等,而且从下标到具体的值走一个o(1)的映射就好了),因为两侧都需要操作候选值,具体为什么是两侧见后续注释
        res = []
        lens = len(nums)

        for right in range(lens):
            # 删除已经离开滑动窗口的下标
            while queue and queue[0] <= right - k:
                queue.popleft()
            # 找到队列中比所有x更小的值并删除. 
            # 这里注意一个逻辑,按照我们刚刚说的,进来一个x,删除掉左侧 / 已经遍历过的所有比x小的数字,所以这个维护的候选集必然是一个不递增的,也就是nums[queue[0]] >= nums[queue[1]] >= nums[queue[2]]. 所以这里从尾部开始遍历
            while queue and nums[queue[-1]] <= nums[right]:
                queue.pop()
            queue.append(right) # 加入维护队列. 这里的不同点是,我自己的思维逻辑是维护旧的,处理新的,但这个代码好像是根据新数字处理旧数字,新的数字下标直接append

            if right >= k - 1:
                res.append(nums[queue[0]])
        return res

注意状态更新那里的那个注释,我和GPT理了一下逻辑:

维护旧状态和处理新状态并不是二选一的关系,更合理的表达或者说是逻辑是:"用新的元素更新旧状态,再把新的元素加入状态"。

判断的逻辑应该是:

新元素是否能够证明某些旧元素以后永远不可能产生更优答案?

如果可以,就利用新元素去更新旧的状态(例如本题的更新就是删除 / 过滤);如果不能,就必须继续保存旧元素,可能需要堆、平衡树或完整历史记录。

而且在本题当中,新元素虽然不一定马上是最大候选值,但必须加入维护 / 队列的原因是:

  1. 它可能比未来的队尾候选大。
  2. 它的位置更新,因此比旧元素更晚过期。

在本代码中,queue这里实际保存的是下标,为了直观显示对应数值。

两段删除分别负责不同事情:

复制代码
while queue and queue[0] <= right - k:
    queue.popleft()

负责删除已经离开窗口的元素。

复制代码
while queue and nums[queue[-1]] <= nums[right]:
    queue.pop()

负责删除仍在窗口中、但已经不可能成为未来最大值的元素。

每个下标最多入队一次、出队一次,所以:

复制代码
时间复杂度:O(n)
空间复杂度:O(k)
相关推荐
爱敲键盘的猴子几秒前
Spring MVC 详解(一):全注解开发与请求响应处理
java·spring·mvc
counting money9 分钟前
SpringBoot 项目创建(IDEA不全,还需要修改)
java·spring boot·spring·maven
Dr.kangder9 分钟前
嵌入式面试总结(二十二)——指针
java·面试·职场和发展·架构·嵌入式
MacroZheng14 分钟前
几行代码给项目集成AI功能,Spring AI 2.0太香了!
java·人工智能·spring boot
Nil20832 分钟前
leetcode 48旋转图像
算法·leetcode·职场和发展
Devin~Y42 分钟前
从内容社区到AI智能客服:Spring Boot + Spring Cloud + Spring AI 全栈实战面试拆解
java·spring boot·redis·elasticsearch·spring cloud·kafka·mybatis
未秃头的程序猿43 分钟前
从写CRUD到做AI Agent:我花了6个月转型,这是我的完整路线图
java·后端·ai编程
Java内核笔记43 分钟前
万字长文剖析 Spring Boot 4.1.0 启动流程源码:从 main 到就绪
java·后端
2602_959960921 小时前
电商大厂Java面试:从Spring Boot、JPA、微服务到Redis、Kafka、Spring Security与监控,谢飞机的爆笑三轮问答
java·jvm·spring boot·redis·面试题
嘟嘟07171 小时前
顺时针螺旋填充 n×n 矩阵:手撕 generateMatrix 的四个 for 循环
javascript·算法·面试