LeetCode 热题 100 之第11题 滑动窗口最大值(JavaScript篇)

10 阅读4分钟

传送门:239. 滑动窗口最大值 - 力扣(LeetCode)

🧩 题目

给你一个整数数组 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 个索引)