Leetcode 239. 滑动窗口最大值
侧边栏壁纸
  • 累计撰写 11 篇文章
  • 累计收到 1 条评论

Leetcode 239. 滑动窗口最大值

tqtqtq
2026-04-16 / 0 评论 / 2 阅读 / 正在检测是否收录...

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 <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

题解笔记:暴力、大顶堆、单调队列

一、先理解题目本质

这道题的核心是:

  • 窗口大小固定为 k
  • 窗口不断向右滑动
  • 每次都要快速得到当前窗口的最大值

最直接的想法是:
每形成一个窗口,就把窗口里的元素都扫一遍,找最大值。

这就是暴力解法。

但如果数组很长,每次都重新扫,会有很多重复计算,所以就会继续优化到:

  • 大顶堆
  • 单调队列

解法一:暴力解法

思路

每次窗口形成后,直接遍历这个窗口里的所有元素,找到最大值。

例如窗口是:

[3, -1, -3]

那就把这 3 个数重新比较一遍,得到最大值 3

代码

from typing import List

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        ans = []

        for left in range(len(nums) - k + 1):
            max_num = nums[left]
            for right in range(left, left + k):
                max_num = max(max_num, nums[right])
            ans.append(max_num)

        return ans

复杂度

  • 时间复杂度:O(n * k)
  • 空间复杂度:O(1)(不算返回结果)

优缺点

优点

  • 最容易理解
  • 非常适合刚开始学这题时打基础

缺点

  • 每个窗口都重新扫描一遍
  • 重复计算很多
  • 数据大时容易超时

解法二:大顶堆

先说结论

大顶堆可以做,而且比暴力解法更高效。
不过它不是最优解,通常复杂度是:

  • 时间复杂度:O(n log n)(严格说更稳妥)
  • 空间复杂度:O(n)

1. 为什么会想到堆

因为这题每次都在问:

当前窗口里的最大值是谁?

而堆最擅长做的事就是:

  • 插入一个数
  • 快速拿到最大值(或最小值)

所以一个很自然的想法就是:

  • 把窗口相关元素放进堆里
  • 每次从堆顶拿最大值

2. 但为什么不能直接只存值

问题在于:
窗口会向右移动,旧元素会离开窗口。

例如:

[3, -1, -3]

下一步变成:

[-1, -3, 5]

这里的 3 已经离开窗口了。
但如果它还在堆里,并且还在堆顶,那就会影响答案。

所以只存值不够,还必须存:

  • 下标

3. 为什么 Python 里要存 (-值, 下标)

因为 Python 的 heapq 默认是小顶堆,没有原生大顶堆。

所以我们常用一个技巧:

  • 把值取负
  • 原本最大的值,负数后反而最小
  • 这样小顶堆就能模拟大顶堆

例如数字 5,下标为 4,会存成:

(-5, 4)

其中:

  • -5:用来模拟大顶堆
  • 4:表示它原来的下标

4. 什么叫“过期元素”

假设当前窗口范围是:

[left, right]

如果某个元素的下标:

idx < left

就说明它已经滑出窗口,不能再参与当前窗口最大值比较了。

这类元素就叫:

过期元素

5. 什么叫“懒删除”

堆不擅长删除中间任意位置的元素。

所以我们不主动去堆里搜索并删除所有过期元素,而是:

  • 只要堆顶过期,就把堆顶弹出
  • 一直弹到堆顶合法为止

这就叫:

懒删除

代码里通常是:

while heap[0][1] < left:
    heapq.heappop(heap)

代码

from typing import List
import heapq

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        heap = []
        ans = []

        for i, num in enumerate(nums):
            # 入堆:(-值, 下标)
            heapq.heappush(heap, (-num, i))

            # 窗口形成后再开始记录答案
            if i >= k - 1:
                left = i - k + 1

                # 懒删除:弹掉所有过期堆顶
                while heap[0][1] < left:
                    heapq.heappop(heap)

                # 堆顶就是当前窗口最大值
                ans.append(-heap[0][0])

        return ans
堆解法动画

复杂度

  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)

优缺点

优点

  • 思路比单调队列更直观
  • 很适合从暴力解法继续进阶
  • 也能解决这道题

缺点

  • 不是最优解
  • 代码里要理解“过期元素”和“懒删除”
  • 堆中可能保留一些已经过期但暂时不在堆顶的元素

解法三:单调队列

先说结论

这是这道题的最优解。

  • 时间复杂度:O(n)
  • 空间复杂度:O(k)

1. 核心思想

我们希望队列里维护的是:

可能成为当前窗口最大值的元素下标

并且让这些下标对应的值,保持单调递减

也就是说:

  • 队首最大
  • 队尾最小

这样每次窗口形成后:

  • 队首就是当前窗口最大值的下标
  • 直接取 nums[deque[0]] 就行

2. 为什么要存下标,不直接存值

因为窗口会移动,我们需要判断队首元素是否已经离开窗口。

如果只存值,就不知道它对应的是哪个位置。

所以单调队列里存的是:

下标

3. 队列维护规则

遍历到 i 时,做三件事。

第一步:移除队首过期元素

如果队首下标已经不在窗口内:

deque[0] < i - k + 1

就把它弹掉。

第二步:保持单调递减

如果当前元素 nums[i] 比队尾对应的值还大,那么队尾那个元素以后就不可能成为最大值了。

因为:

  • 它比当前元素小
  • 它还比当前元素更早过期

所以直接弹出队尾,直到队列重新单调递减。

第三步:把当前下标加入队尾

这样队列中保留的,都是未来仍有机会成为最大值的元素。

代码

from typing import List
from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        q = deque()
        ans = []

        for i, num in enumerate(nums):
            # 1. 删除窗口左边已经过期的下标
            while q and q[0] < i - k + 1:
                q.popleft()

            # 2. 维护单调递减队列
            while q and nums[q[-1]] < num:
                q.pop()

            # 3. 当前下标加入队尾
            q.append(i)

            # 4. 窗口形成后,队首就是最大值
            if i >= k - 1:
                ans.append(nums[q[0]])

        return ans

复杂度

  • 时间复杂度:O(n)
  • 空间复杂度:O(k)

为什么是 O(n)

因为每个元素最多只会:

  • 进队一次
  • 出队一次

不会反复进出,所以总操作次数和 n 成正比。

优缺点

优点

  • 最优解
  • 性能最好
  • 是这道题的经典标准答案

缺点

  • 对初学者来说抽象一点
  • 第一次接触“单调队列”时不如堆直观
0

评论 (0)

取消