239. 滑动窗口最大值 
题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
你只可以看到窗口内的 k 个数字。滑动窗口每次只向右移动一位。
请返回滑动窗口中的最大值。
示例:
1. 题型判断
本题要求每个长度为 k 的连续子数组中的最大值。
因为窗口长度固定,并且窗口会从左到右每次滑动一格,所以这是一个典型的固定长度滑动窗口问题。
普通滑动窗口只需要维护窗口边界,但这道题还要快速拿到窗口最大值。如果每次窗口移动后都重新遍历 k 个元素找最大值,时间复杂度会变成 O(n * k),容易超时。
因此需要在滑动窗口基础上使用单调队列维护最大值。
2. 指针含义
使用闭区间窗口 [left, right]。
right:当前遍历到的元素下标,负责向右扩展窗口。left:当前窗口左边界,固定长度窗口中可以由right - k + 1得到。deque:单调递减队列,保存数组下标。ans:保存每个窗口的最大值。
队列里存的是下标,而不是值。这样既能通过 nums[deque[0]] 得到当前最大值,也能判断队头元素是否已经离开窗口。
单调队列满足:
所以队头下标对应的元素,就是当前窗口内的最大值。
3. 窗口或区间维护规则
每次遍历到 right 时,需要做三件事。
第一,移除已经离开窗口的下标。
当队头下标 deque[0] <= right - k 时,说明它已经不在当前窗口 [right - k + 1, right] 中,需要从队头删除。
第二,维护队列单调递减。
如果当前元素 nums[right] 大于等于队尾元素 nums[deque[deque.length - 1]],那么队尾元素不可能再成为后续窗口最大值:
- 当前元素更大,位置还更靠右。
- 只要队尾元素还在窗口里,当前元素也一定在窗口里。
所以可以不断弹出队尾。
第三,把当前下标加入队尾。
完成这三步后,队头就是当前窗口的最大值下标。
4. 答案更新时机
本题是固定长度窗口。
只有当窗口长度达到 k 时,才开始记录答案。
遍历到 right 时,如果 right >= k - 1,说明第一个完整窗口已经形成,此时可以把队头对应的值加入答案:
注意答案要在完成过期元素移除、单调队列维护、当前元素入队之后更新。这样 deque[0] 一定是当前窗口内的最大值。
5. 边界条件与易错点
边界条件:
- 如果
nums.length === 0,返回空数组。 - 如果
k === 1,每个窗口只有一个元素,结果就是原数组。 - 如果
k === nums.length,结果只有一个值,即整个数组最大值。
易错点:
- 单调队列建议存下标,不要只存值。因为窗口滑动时需要判断队头元素是否过期。
- 移除过期元素的条件是
deque[0] <= right - k,表示该下标已经在当前窗口左侧。 - 队列维护的是单调递减,不是普通栈;队头是最大值,队尾用于插入新元素。
- 固定窗口长度达到
k后再更新答案,也就是right >= k - 1。 - 如果使用
shift(),语义最直观;在 JavaScript 中极端数据下可能有移动成本,也可以用队头指针优化。
错误思路示例:
这种写法每个窗口都重新扫描一遍,时间复杂度是 O(n * k)。虽然逻辑简单,但没有利用相邻窗口之间只有一个元素进、一个元素出的特点。
6. 代码实现
写法一:单调队列
写法二:队头指针优化
Array.prototype.shift() 会移动数组元素。为了让每次队头删除也保持常数级操作,可以用 head 表示当前有效队头位置。
7. 复杂度分析
写法一:
- 时间复杂度:理论上每个下标最多入队一次、出队一次,单调队列逻辑是
O(n);但 JavaScript 的shift()可能触发元素移动,极端情况下会带来额外开销。 - 空间复杂度:
O(k),队列中最多保存当前窗口相关的下标。
写法二:
- 时间复杂度:
O(n),每个下标最多入队一次、出队一次。 - 空间复杂度:
O(k),队列最多保存窗口内可能成为最大值的下标。

