LeetCode 热题 HOT 100(子串)239. 滑动窗口最大值

63 阅读4分钟

题目简介

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

解题思路

使用双端队列以单调递减的方式记录窗口内出现的元素下标,通过下标可以便捷判断某个元素是否还在窗口内。每次遍历新元素时,将双端队列中比这个元素小的元素下标弹出后将新元素下标加入,在形成窗口时, 队首就是当前窗口最大元素的下标。在前进过程中,判断队首元素是否已经不在最新的窗口内,因为根据 i 可以计算出最新窗口的合法左边界,和队首元素(下标)对比即可知。

解题思路:滑动窗口最大值

这道题要求在大小为 k 的滑动窗口中找出最大值。我们有几种解法,其中最优的是使用双端队列(Deque)实现的单调队列。

思路分析

  1. 暴力解法:对每个滑动窗口,遍历k个元素找出最大值,时间复杂度O(n*k),不够高效。

  2. 单调队列解法:维护一个双端队列,队列中存储的是元素下标,并且保证队列中的元素对应的值是单调递减的。

    • 队头始终是当前窗口最大值的下标
    • 当队头元素已经不在当前窗口范围内时,将其弹出
    • 新元素入队前,从队尾移除所有小于当前元素的值
    • 这样保证队列中的元素从队头到队尾单调递减,队头即为窗口最大值

代码实现

func maxSlidingWindow(nums []int, k int) []int {
    if len(nums) < k {
        return nil
    }
    
    n := len(nums)
    result := make([]int, n-k+1)
    dequeue := make([]int, 0)
    for i := 0; i < n; i++ {
        // 1. 删除非窗口内下标, 窗口起始边界下标为 i-k+1
        if len(dequeue) > 0 && dequeue[0] < i-k+1 {
            dequeue = dequeue[1:]
        }
        // 2. 删除小于当前元素的下标
        for len(dequeue) > 0 && nums[dequeue[len(dequeue)-1]] < nums[i] {
            dequeue = dequeue[:len(dequeue)-1]
        }
        dequeue = append(dequeue, i)
        if i >= k-1 {
            result[i-k+1] = nums[dequeue[0]]
        }
    }
    return result
}

复杂度分析

  • 时间复杂度:O(n),其中 n 是数组长度。每个元素最多入队和出队一次。
  • 空间复杂度:O(k),队列最大长度不超过 k。

算法执行步骤示例

以输入 nums = [1,3,-1,-3,5,3,6,7], k = 3 为例,执行过程如下:

当前元素 i窗口状态双端队列(下标)双端队列对应的值最大值结果数组
0: 1[1][0][1]-[]
1: 3[1,3][1][3]-[]
2: -1[1,3,-1][1,2][3,-1]3[3]
3: -3[3,-1,-3][1,2,3][3,-1,-3]3[3,3]
4: 5[-1,-3,5][4][5]5[3,3,5]
5: 3[-3,5,3][4,5][5,3]5[3,3,5,5]
6: 6[5,3,6][6][6]6[3,3,5,5,6]
7: 7[3,6,7][7][7]7[3,3,5,5,6,7]

单调队列的关键点

  1. 为什么使用单调队列?

    • 它能在O(1)时间内获取窗口最大值
    • 每个元素最多入队和出队一次,总操作次数为O(n)
  2. 单调队列的维护

    • 移除不在窗口范围内的元素(保证队列中元素都在当前窗口内)
    • 移除小于当前元素的所有元素(保证队列单调递减)
  3. 存储下标而非值的原因

    • 通过下标可以判断元素是否在当前窗口内
    • 同时也可以通过下标获取对应的值

通过使用单调队列,我们能够以O(n)的时间复杂度解决这个问题,远优于暴力解法的O(n*k)。

完整处理状态变化图

graph LR
    subgraph S1[i=0]
        A1["nums=[1,3,-1,-3,5,3,6,7]"] --> |"添加1"| B1["deque=[0]"]
    end

    subgraph S2[i=1]
        A2["nums=[1,3,-1,-3,5,3,6,7]"] --> |"3>1"| B2["deque=[1]"]
    end

    subgraph S3[i=2]
        A3["nums=[1,3,-1,-3,5,3,6,7]"] --> |"窗口[1,3,-1]"| B3["deque=[1,2]"]
        B3 --> C3["result=[3]"]
    end

    subgraph S4[i=3]
        A4["nums=[1,3,-1,-3,5,3,6,7]"] --> |"窗口[3,-1,-3]"| B4["deque=[2,3]"]
        B4 --> C4["result=[3,3]"]
    end

    subgraph S5[i=4]
        A5["nums=[1,3,-1,-3,5,3,6,7]"] --> |"窗口[-1,-3,5]"| B5["deque=[4]"]
        B5 --> C5["result=[3,3,5]"]
    end

    subgraph S6[i=5]
        A6["nums=[1,3,-1,-3,5,3,6,7]"] --> |"窗口[-3,5,3]"| B6["deque=[4,5]"]
        B6 --> C6["result=[3,3,5,5]"]
    end

    subgraph S7[i=6]
        A7["nums=[1,3,-1,-3,5,3,6,7]"] --> |"窗口[5,3,6]"| B7["deque=[6]"]
        B7 --> C7["result=[3,3,5,5,6]"]
    end

    subgraph S8[i=7]
        A8["nums=[1,3,-1,-3,5,3,6,7]"] --> |"窗口[3,6,7]"| B8["deque=[7]"]
        B8 --> C8["result=[3,3,5,5,6,7]"]
    end

    S1 --> S2
    S2 --> S3
    S3 --> S4
    S4 --> S5
    S5 --> S6
    S6 --> S7
    S7 --> S8

    style A1 fill:#e3f2fd
    style A2 fill:#e3f2fd
    style A3 fill:#e3f2fd
    style A4 fill:#e3f2fd
    style A5 fill:#e3f2fd
    style A6 fill:#e3f2fd
    style A7 fill:#e3f2fd
    style A8 fill:#e3f2fd
    
    style B1 fill:#f3e5f5
    style B2 fill:#f3e5f5
    style B3 fill:#f3e5f5
    style B4 fill:#f3e5f5
    style B5 fill:#f3e5f5
    style B6 fill:#f3e5f5
    style B7 fill:#f3e5f5
    style B8 fill:#f3e5f5
    
    style C3 fill:#e8f5e9
    style C4 fill:#e8f5e9
    style C5 fill:#e8f5e9
    style C6 fill:#e8f5e9
    style C7 fill:#e8f5e9
    style C8 fill:#e8f5e9