🧩 题目
给你一个整数数组 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
🛠️解题代码
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
const result = [];
const deque = new Deque();
for (let rightIndex = 0; rightIndex < nums.length; rightIndex++) {
// 1. 维护单调递减队列:从右侧入队当前元素索引
while (!deque.isEmpty() && nums[deque.back()] <= nums[rightIndex]) {
deque.popBack(); // 移除比当前元素小的元素,保持单调递减
}
deque.pushBack(rightIndex);
// 2. 计算窗口左边界
const leftIndex = rightIndex - k + 1;
// 3. 如果队列头部元素已经滑出当前窗口,移除它
if (deque.front() < leftIndex) {
deque.popFront();
}
// 4. 当窗口有效时(即窗口已形成),记录当前窗口最大值
if (leftIndex >= 0) {
result.push(nums[deque.front()]);
}
}
return result;
};
🧠 算法思想:单调队列维护滑动窗口最大值
核心思路:
我们希望在一个动态变化的窗口中,快速获取当前窗口的最大值。
直接每次遍历窗口查找最大值的时间复杂度是 O(nk),效率较低。
我们使用一个 单调递减双端队列(Monotonic Deque) 来维护当前窗口中可能成为最大值的元素索引,使得:
- 队列头部始终保存当前窗口中的最大值索引;
- 队列保持单调递减顺序;
- 每个元素最多入队、出队一次,整体时间复杂度为 O(n)。
📌 函数签名说明:
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
nums
:输入数组。k
:滑动窗口大小。- 返回值:每个窗口的最大值组成的数组。
🧱 变量定义与初始化:
const result = [];
const deque = new Deque();
result
:存储每个窗口的最大值。deque
:单调递减双端队列,用于保存当前窗口中可能成为最大值的元素的索引。
🧱 主循环逻辑详解:
for (let rightIndex = 0; rightIndex < nums.length; rightIndex++) {
- 使用变量
rightIndex
表示窗口右边界(当前处理的元素下标)。 - 整个数组只遍历一次。
✅ 步骤一:维护单调递减队列(入队)
while (!deque.isEmpty() && nums[deque.back()] <= nums[rightIndex]) {
deque.popBack(); // 移除比当前元素小的元素,保持单调递减
}
deque.pushBack(rightIndex);
🔍 作用:
- 保证
deque
中保存的索引对应的元素是单调递减的。 - 如果当前元素
nums[rightIndex]
大于队尾元素,则弹出队尾元素,因为它们不可能再成为后续窗口的最大值。 - 最后将当前索引加入队列尾部。
🧠 举例:
假设 nums = [3, 1, 8, 7]
,当处理到 8
时:
- 队列中有
[0,1]
(对应元素 3 和 1) - 因为 8 > 1,所以弹出 1;8 > 3,也弹出 3
- 队列变为
[]
,然后加入2
- 此时队列中只有
2
,表示 8 是当前最大值
✅ 步骤二:计算窗口左边界
const leftIndex = rightIndex - k + 1;
- 计算当前窗口的左边界索引
leftIndex
。 - 窗口范围为
[leftIndex, rightIndex]
,长度为k
。
✅ 步骤三:判断队首是否已滑出窗口(出队)
if (deque.front() < leftIndex) {
deque.popFront();
}
🔍 作用:
- 如果队列头部元素的索引已经小于当前窗口左边界,说明它已经不在当前窗口内,需要将其移除。
- 这样可以确保队列头部始终指向当前窗口内的最大值。
✅ 步骤四:记录窗口最大值
if (leftIndex >= 0) {
result.push(nums[deque.front()]);
}
🔍 作用:
- 当窗口形成(即
leftIndex >= 0
)时,将当前窗口最大值(即队列头部元素)加入结果数组。 - 否则(如前
k-1
次循环),不记录结果。
📊 示例演示
以 nums = [1,3,-1,-3,5,3,6,7], k = 3
为例:
i = 0 → nums[i] = 1 → deque = [0] left = -2 → 不记录
i = 1 → nums[i] = 3 → deque = [1] left = -1 → 不记录
i = 2 → nums[i] =-1 → deque = [1,2] left = 0 → 记录 3
i = 3 → nums[i] =-3 → deque = [1,2,3] left = 1 → 记录 3
i = 4 → nums[i] = 5 → deque = [4] left = 2 → 记录 5
i = 5 → nums[i] = 3 → deque = [4,5] left = 3 → 记录 5
i = 6 → nums[i] = 6 → deque = [6] left = 4 → 记录 6
i = 7 → nums[i] = 7 → deque = [7] left = 5 → 记录 7
最终输出:[3, 3, 5, 5, 6, 7]
⏱️ 时间复杂度分析
- 每个索引最多入队、出队一次 → 每个操作 O(1)
- 整体遍历一次数组 → O(n)
✅ 总时间复杂度:O(n)
✅ 空间复杂度:O(k)(队列最多存 k 个索引)