题目简介
给你一个整数数组 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] <= 1041 <= k <= nums.length
解题思路
使用双端队列以单调递减的方式记录窗口内出现的元素下标,通过下标可以便捷判断某个元素是否还在窗口内。每次遍历新元素时,将双端队列中比这个元素小的元素下标弹出后将新元素下标加入,在形成窗口时, 队首就是当前窗口最大元素的下标。在前进过程中,判断队首元素是否已经不在最新的窗口内,因为根据 i 可以计算出最新窗口的合法左边界,和队首元素(下标)对比即可知。
解题思路:滑动窗口最大值
这道题要求在大小为 k 的滑动窗口中找出最大值。我们有几种解法,其中最优的是使用双端队列(Deque)实现的单调队列。
思路分析
-
暴力解法:对每个滑动窗口,遍历k个元素找出最大值,时间复杂度O(n*k),不够高效。
-
单调队列解法:维护一个双端队列,队列中存储的是元素下标,并且保证队列中的元素对应的值是单调递减的。
- 队头始终是当前窗口最大值的下标
- 当队头元素已经不在当前窗口范围内时,将其弹出
- 新元素入队前,从队尾移除所有小于当前元素的值
- 这样保证队列中的元素从队头到队尾单调递减,队头即为窗口最大值
代码实现
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] |
单调队列的关键点
-
为什么使用单调队列?
- 它能在O(1)时间内获取窗口最大值
- 每个元素最多入队和出队一次,总操作次数为O(n)
-
单调队列的维护:
- 移除不在窗口范围内的元素(保证队列中元素都在当前窗口内)
- 移除小于当前元素的所有元素(保证队列单调递减)
-
存储下标而非值的原因:
- 通过下标可以判断元素是否在当前窗口内
- 同时也可以通过下标获取对应的值
通过使用单调队列,我们能够以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