LeetCode - Hot 100 - 滑动窗口最大值

算法每日一题 | LeetCode 239:滑动窗口最大值

📌 题目描述

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

返回滑动窗口中的最大值。

示例 1:

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

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

🧠 思路分析

这题乍一看挺唬人的------窗口每滑一格,我都得在 k 个元素里找最大值。最朴素的做法就是每到一个新位置就遍历一遍窗口,时间复杂度 O(n×k)O(n \times k) O(n×k),数据量一大直接 TLE。

那问题的关键就变成了:怎么在窗口滑动的过程中,快速知道当前最大值是谁?

答案是一个叫 单调队列 的数据结构。名字听着挺高级的,其实说白了就是一个双端队列(Deque),只不过里面存的元素下标对应的值是单调递减的。

为什么是单调递减?因为窗口往右滑的时候,如果一个旧元素比新来的元素小,那这个旧元素永远不可能是最大值了------新来的比它大,而且新来的在窗口里待得还比它久。所以这些"没希望了"的元素可以直接踢掉。

具体来说,这个单调队列里存的是数组下标(不是值本身),维护三件事:

  1. 踢过期:窗口滑走了,队头下标已经不在窗口内了,直接移除
  2. 踢弱者:新来的元素如果比队尾的元素大,就把队尾踢掉,循环踢,直到队尾比新来的大或者队空了
  3. 入队 + 取最大值:把新下标加到队尾,这时候队头就是当前窗口的最大值

举个例子,nums = [1,3,-1,-3,5,3,6,7]k = 3

ini 复制代码
i=0, nums[0]=1:  队空,直接入队。deque=[0]
i=1, nums[1]=3:  3 > nums[0]=1,踢掉0,入队。deque=[1]
i=2, nums[2]=-1: -1 < nums[1]=3,不用踢,入队。deque=[1,2]
                 i>=2了,窗口成形,max = nums[deque.peekFirst()] = nums[1] = 3 ✓

i=3, nums[3]=-3: -3 < nums[2]=-1,不用踢,入队。deque=[1,2,3]
                 max = nums[1] = 3 ✓

i=4, nums[4]=5:  先检查队头:deque.peekFirst()=1, 1 <= 4-3=1,过期了!踢掉。deque=[2,3]
                 然后 5 > nums[3]=-3,踢;5 > nums[2]=-1,踢。deque=[]
                 入队。deque=[4]
                 max = nums[4] = 5 ✓

i=5, nums[5]=3:  3 < nums[4]=5,不用踢,入队。deque=[4,5]
                 max = nums[4] = 5 ✓

i=6, nums[6]=6:  队头4 <= 6-3=3?4 > 3,没过期的。
                 6 > nums[5]=3,踢;6 > nums[4]=5,踢。deque=[]
                 入队。deque=[6]
                 max = nums[6] = 6 ✓

i=7, nums[7]=7:  队头6 <= 7-3=4?6 > 4,没过期。
                 7 > nums[6]=6,踢。deque=[]
                 入队。deque=[7]
                 max = nums[7] = 7 ✓

结果: [3, 3, 5, 5, 6, 7]

整个过程中,每个元素最多入队一次、出队一次,所以总的时间复杂度是 O(n)O(n) O(n),比暴力的 O(nk)O(nk) O(nk) 好太多了。


🖼️ 图解与执行流程

nums = [1,3,-1,-3,5,3,6,7]k = 3 来画个过程图:

ini 复制代码
窗口位置             deque 内容(下标)      队头=最大值    输出
─────────────────────────────────────────────────────────────
[1  3  -1]           [1, 2]               nums[1]=3     3
 1 [3  -1  -3]       [1, 2, 3]            nums[1]=3     3
 1  3 [-1  -3  5]    [4]                  nums[4]=5     5
 1  3  -1 [-3  5  3] [4, 5]               nums[4]=5     5
 1  3  -1  -3 [5  3  6] [6]               nums[6]=6     6
 1  3  -1  -3  5 [3  6  7] [7]            nums[7]=7     7

                                      结果: [3, 3, 5, 5, 6, 7]

队列里的下标对应的值永远是从大到小排列的------这就是"单调递减队列"的含义。队头永远是当前窗口的老大(最大值)。


💻 核心代码实现

java 复制代码
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int length = nums.length;
        // 结果数组大小 = 数组长度 - 窗口大小 + 1
        int[] res = new int[length - k + 1];
        // 双端队列,存的是数组下标,不是值
        Deque<Integer> deque = new LinkedList<>();

        for (int i = 0; i < length; i++) {
            // 第一步:踢掉过期的队头
            // 如果队头下标已经不在当前窗口 [i-k+1, i] 范围内了,就移除它
            if (!deque.isEmpty() && deque.peekFirst() <= i - k) {
                deque.pollFirst();
            }

            // 第二步:踢掉所有比当前元素小的队尾元素
            // 维护单调递减性质:新来的比队尾大,队尾就永远不可能当最大值了
            while (!deque.isEmpty() && nums[deque.peekLast()] <= nums[i]) {
                deque.pollLast();
            }

            // 第三步:把当前下标加入队尾
            deque.offerLast(i);

            // 第四步:当窗口完全成形(i >= k-1),记录当前窗口的最大值
            // 队头下标对应的值就是最大值
            if (i >= k - 1) {
                res[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        return res;
    }
}

代码结构很清晰------循环里就四步:踢过期、踢弱者、入队、取最大值。其中"踢弱者"用的是 while 循环,因为新来的元素可能比队列里好几个都大,得一直踢到队尾比它大为止。

复杂度分析

  • 时间复杂度 O(n)O(n) O(n),虽然有内外两层循环,但每个下标最多入队一次、出队一次,摊下来是线性的
  • 空间复杂度 O(k)O(k) O(k),队列里最多同时存 k 个下标(窗口大小)

⚠️ 踩坑记录

  1. 队列里存下标,不是存值 :这点非常关键。存值的话你没法判断元素是不是过期了(滑出窗口了)。只有存下标,才能通过 下标 <= i - k 来判断是否该踢
  2. 过期检查是 <= i - k 不是 < i - k :窗口左边界是 i - k + 1,所以下标 <= i - k 的就已经在窗口外面了。这里差一个 1 就会出 bug
  3. 踢弱者用的是 <= 不是 <nums[deque.peekLast()] <= nums[i] 这里等于号也要踢。因为相等的时候,新来的在窗口里待得更久,旧的没用了
  4. 取最大值的时机 :只有当 i >= k - 1 的时候窗口才完整,之前窗口还没凑够 k 个元素,不要往结果里写

🔗 LeetCode 原题链接

LeetCode 239. 滑动窗口最大值

相关推荐
8Qi83 小时前
LeetCode 300 & 674:最长递增子序列 vs 最长连续递增子序列
算法·leetcode·职场和发展·动态规划
sheeta19983 小时前
LeetCode 补拙笔记 日期:2026.06.07 题目:283. 移动零
笔记·算法·leetcode
8Qi83 小时前
LeetCode 188 & 123:股票买卖问题(限制交易次数)—— 联合题解
算法·leetcode·职场和发展·动态规划
一只齐刘海的猫4 小时前
【Leetcode】三数之和
数据结构·算法·leetcode
sheeta19984 小时前
LeetCode 补拙笔记 日期:2026.06.07 题目:49. 字母异位词分组
笔记·算法·leetcode
ysu_03144 小时前
leetcode数据结构与算法5~7:链表双指针与二级指针
数据结构·学习·算法·leetcode·链表
小欣加油4 小时前
leetcode542 01矩阵
数据结构·c++·算法·leetcode·矩阵·bfs
想吃火锅10055 小时前
【leetcode】3.无重复字符的最长字串js版
算法·leetcode·职场和发展
fengxin_rou5 小时前
LeetCode链表经典五题:从相交到环形,双指针的妙用
算法·leetcode·链表