【Hot100|13-LeetCode 56. 合并区间】

LeetCode 239. 滑动窗口最大值 - 单调队列解法详解

一、问题理解

问题描述

给定一个整数数组 nums 和一个整数 k,滑动窗口从数组的最左侧移动到最右侧,每次只向右移动一位。请找出所有滑动窗口中的最大值,并返回这些最大值组成的数组。

示例

text

输入: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 二、核心思路:单调队列维护潜在最大值 暴力解法的局限性 对于每个窗口都重新遍历 k 个元素找最大值,时间复杂度为 O(nk),效率极低。 单调队列优化思路 单调队列定义:使用双端队列(Deque)维护一个单调递减的队列,存储元素的索引。 队列特性: 队列中的索引对应的元素值从队首到队尾单调递减。 队首元素总是当前窗口的最大值。 维护操作: 入队:新元素入队时,从队尾开始移除所有小于等于它的元素,然后入队。 出队:检查队首元素是否还在当前窗口内,如果不在则移除。 获取最大值:窗口完全形成后,队首元素即为当前窗口最大值。 三、代码逐行解析 Java 解法 java import java.util.ArrayDeque; import java.util.Deque; class Solution { public int\[\] maxSlidingWindow(int\[\] nums, int k) { int n = nums.length; // 1. 结果数组:滑动窗口的个数为 n - k + 1 int\[\] ans = new int\[n - k + 1\]; // 2. 双端队列:存储元素索引,维护队列内元素值单调递减 Deque\ q = new ArrayDeque\<\>(); // 3. 遍历数组的每个元素 for (int i = 0; i \< n; i++) { // 3.1 新元素从队尾入队,维护队列单调性 // 从队尾开始,移除所有小于等于当前元素的索引 // 因为这些元素不可能成为后续窗口的最大值 while (!q.isEmpty() \&\& nums\[q.getLast()\] \<= nums\[i\]) { q.removeLast(); } // 将当前元素索引加入队尾 q.addLast(i); // 3.2 移除窗口外的元素 // 计算当前窗口的左边界 int left = i - k + 1; // 如果队首索引小于左边界,说明队首元素不在当前窗口内 if (q.getFirst() \< left) { q.removeFirst(); } // 3.3 当窗口完全形成时,记录当前窗口最大值 // 当 left \>= 0 时,窗口已包含 k 个元素 if (left \>= 0) { ans\[left\] = nums\[q.getFirst()\]; } } // 4. 返回结果 return ans; } } Python 解法 python from collections import deque from typing import List class Solution: def maxSlidingWindow(self, nums: List\[int\], k: int) -\> List\[int\]: n = len(nums) # 1. 边界处理 if n == 0 or k == 0: return \[

2. 初始化结果数组和双端队列

ans = [0] * (n - k + 1)

q = deque()

3. 遍历数组

for i in range(n):

3.1 维护队列单调性

从队尾开始,移除所有小于等于当前元素的索引

while q and nums[q[-1]] <= nums[i]:

q.pop()

将当前索引加入队尾

q.append(i)

3.2 移除窗口外的元素

计算当前窗口的左边界

left = i - k + 1

如果队首索引小于左边界,说明不在当前窗口内

if q[0] < left:

q.popleft()

3.3 记录结果(当窗口完全形成时)

if left >= 0:

ans[left] = nums[q[0]]

4. 返回结果

return ans

四、Java 与 Python 语法对比

  1. 队列操作

操作 Java Python

创建双端队列 Deque<Integer> q = new ArrayDeque<>(); q = deque()

获取队尾元素 q.getLast() q[-1]

移除队尾元素 q.removeLast() q.pop()

获取队首元素 q.getFirst() q[0]

移除队首元素 q.removeFirst() q.popleft()

添加元素到队尾 q.addLast(i) q.append(i)

  1. 数组/列表操作

操作 Java Python

创建数组 int[] ans = new int[n - k + 1]; ans = [0] * (n - k + 1)

获取数组长度 nums.length len(nums)

获取数组元素 nums[i] nums[i]

  1. 循环与控制流

操作 Java Python

for 循环 for (int i = 0; i < n; i++) for i in range(n):

while 循环 while (!q.isEmpty() && ...) while q and ...:

五、实例演示

以测试用例 nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 为例,演示过程:

步骤 i nums[i] 队列操作(维护单调性后) 队列(索引,值) left 队首在窗口内? ans[left]

1 0 1 空队列,直接加入0 [0(1)] -2 不判断 -

2 1 3 1(3) > 0(1),移除0,加入1 [1(3)] -1 不判断 -

3 2 -1 队尾1(3) > -1,直接加入2 [1(3), 2(-1)] 0 是 (1>=0) ans[0]=3

4 3 -3 队尾2(-1) > -3,直接加入3 [1(3), 2(-1), 3(-3)] 1 是 (1>=1) ans[1]=3

5 4 5 依次移除3(-3), 2(-1), 1(3),加入4 [4(5)] 2 是 (4>=2) ans[2]=5

6 5 3 队尾4(5) > 3,直接加入5 [4(5), 5(3)] 3 是 (4>=3) ans[3]=5

7 6 6 移除5(3), 4(5),加入6 [6(6)] 4 是 (6>=4) ans[4]=6

8 7 7 移除6(6),加入7 [7(7)] 5 是 (7>=5) ans[5]=7

最终结果:ans = [3, 3, 5, 5, 6, 7]

六、关键细节解析

  1. 为什么队列存储索引而不是值?

索引可以判断元素是否在窗口内:通过比较索引和窗口左边界,可以知道元素是否已经滑出窗口。

值无法判断位置:如果只存储值,无法知道该值对应的元素是否还在当前窗口内。

  1. 为什么入队时要移除小于等于当前元素的队尾元素?

假设队列尾部有元素 x,当前元素为 y,且 x <= y:

y 比 x 更大(或相等)且更靠右(索引更大)。

在后续窗口中,y 会比 x 更晚离开窗口。

因此,x 永远不可能成为后续窗口的最大值,可以安全移除。

  1. 窗口何时完全形成?

当 i >= k - 1 时,left = i - k + 1 >= 0,此时窗口包含 k 个元素,可以记录最大值。

  1. 为什么队首一定是当前窗口的最大值?

队列维护了从队首到队尾的单调递减性。

队首元素是当前窗口中最早加入队列且未被移除的元素。

通过入队时的筛选,队首元素一定大于队列中其他元素,且在当前窗口内。

七、复杂度分析

时间复杂度:O(n)

每个元素最多入队一次、出队一次。

每个元素的操作次数是常数级别。

总操作次数为 O(n)。

空间复杂度:O(k)

队列中最多同时存储 k 个元素(当数组单调递减时)。

结果数组 O(n-k+1) 不计入空间复杂度(属于输出要求)。

八、优化技巧与变体

  1. 处理边界情况

python

def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

if not nums or k <= 0:

return []

n = len(nums)

if k == 1:

return nums

if k >= n:

return [max(nums)]

后续逻辑...

  1. 使用数组模拟双端队列(优化空间)

python

def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

n = len(nums)

if n == 0 or k == 0:

return []

使用列表模拟双端队列

q = [0] * n # 预分配空间

front, rear = 0, -1 # 队列的头部和尾部指针

ans = [0] * (n - k + 1)

for i in range(n):

维护队列单调性

while front <= rear and nums[q[rear]] <= nums[i]:

rear -= 1

rear += 1

q[rear] = i

移除窗口外的元素

if q[front] < i - k + 1:

front += 1

记录结果

if i >= k - 1:

ans[i - k + 1] = nums[q[front]]

return ans

  1. 使用优先队列(堆)的解法

python

import heapq

from typing import List

class Solution:

def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:

if not nums or k == 0:

return []

n = len(nums)

使用最大堆,存储(-值,索引)对,因为Python的heapq是最小堆

heap = []

result = []

for i in range(n):

将当前元素加入堆中

heapq.heappush(heap, (-nums[i], i))

当窗口完全形成时

if i >= k - 1:

移除堆顶不在窗口内的元素

while heap and heap[0][1] <= i - k:

heapq.heappop(heap)

堆顶元素就是当前窗口的最大值

result.append(-heap[0][0])

return result

复杂度分析:

时间复杂度:O(n log n),每个元素入堆出堆需要 O(log n)

空间复杂度:O(n)

缺点:比单调队列解法慢,但逻辑更简单。

九、常用函数积累

Java 常用函数

java

// 双端队列操作

Deque<Integer> deque = new ArrayDeque<>();

deque.addLast(element); // 添加到队尾

deque.removeLast(); // 移除队尾

deque.getLast(); // 获取队尾

deque.addFirst(element); // 添加到队首

deque.removeFirst(); // 移除队首

deque.getFirst(); // 获取队首

deque.isEmpty(); // 判断队列是否为空

deque.size(); // 获取队列大小

// 数组操作

int[] arr = new int[n];

int length = arr.length; // 数组长度

Arrays.fill(arr, value); // 填充数组

Python 常用函数

python

from collections import deque

双端队列操作

q = deque()

q.append(element) # 添加到队尾

q.pop() # 移除队尾

q[-1] # 获取队尾

q.appendleft(element) # 添加到队首

q.popleft() # 移除队首

q[0] # 获取队首

len(q) # 获取队列大小

bool(q) # 判断队列是否非空

列表操作

arr = [0] * n

len(arr) # 列表长度

max(arr) # 获取最大值

min(arr) # 获取最小值

十、总结

核心要点

单调队列是关键:维护一个单调递减的队列,队首元素始终是当前窗口的最大值。

索引存储很重要:存储索引而非值,可以方便地判断元素是否在窗口内。

时间复杂度优化:从暴力解法的 O(nk) 优化到 O(n)。

面试常见问题

为什么使用单调队列而不是优先队列?

单调队列的均摊时间复杂度是 O(1),而优先队列是 O(log n)。

单调队列更适合滑动窗口问题,因为元素是按顺序加入和移除的。

如何处理数组中有重复元素的情况?

算法天然支持重复元素,因为入队时移除的是小于等于当前元素的元素。

如果有多个相同的最大值,队列中会保留最右侧的那个(索引最大的)。

如果 k 很大,接近 n 怎么办?

算法仍然有效,队列大小最多为 k,空间复杂度 O(k)。

当 k >= n 时,只需要返回整个数组的最大值。

最坏情况下的时间复杂度?

每个元素最多入队一次、出队一次,所以是 O(n)。

如果数组是单调递增的,队列会怎样?

队列中最多只会有一个元素,因为每个新元素都会移除之前的所有元素。

扩展思考

类似问题:

滑动窗口最小值(只需将单调递减改为单调递增)

滑动窗口的中位数(需要更复杂的数据结构)

滑动窗口的平均值(更简单,只需维护窗口和)

变体问题:

限制大小的队列最大值(队列有最大容量,需要支持push和pop)

二维滑动窗口最大值(更复杂,需要结合单调队列和动态规划)

实际应用:

股票价格分析(寻找一段时间内的最高价)

网络流量监控(统计固定时间窗口内的最大流量)

图像处理(滑动窗口滤波器)

掌握单调队列的解法,不仅能够解决滑动窗口最大值问题,还能够解决一系列类似的区间最值问题,是面试中非常重要的算法技巧。


版权声明:本文为CSDN博主「好学且牛逼的马」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/King_model/article/details/154684394

相关推荐
2401_891450461 小时前
基于C++的游戏引擎开发
开发语言·c++·算法
DLGXY1 小时前
数据结构——快慢指针(十五)
数据结构
@Aurora.1 小时前
优选算法【专题六_模拟】
算法
MicroTech20251 小时前
微算法科技(NASDAQ :MLGO)探索量子Hadamard门技术,增强量子图像处理效率
图像处理·科技·算法
进击的小头1 小时前
移动平均滤波器:从原理到DSP ADC采样实战(C语言实现)
c语言·开发语言·算法
历程里程碑1 小时前
Linux 6 权限管理全解析
linux·运维·服务器·c语言·数据结构·笔记·算法
历程里程碑2 小时前
双指针--双数之和
开发语言·数据结构·c++·算法·排序算法·哈希算法·散列表
123_不打狼2 小时前
词嵌入模型
人工智能·算法
拼好饭和她皆失2 小时前
图论:拓扑排序讲解,以及 Dijkstra算法,Bellman-Ford算法,spfa算法,Floyd算法模板大全
算法·图论·最短路