doocs/leetcode 单调队列实战:从滑动窗口到最优解
引言:为什么需要单调队列?
在算法竞赛和实际开发中,我们经常遇到需要维护一个数据结构的"窗口"或"区间"的问题。传统的暴力解法时间复杂度往往无法满足大规模数据的需求,这时单调队列(Monotonic Queue)就成为了解决这类问题的利器。
单调队列是一种特殊的双端队列(Deque),它能够在O(1)或O(n)的时间复杂度内维护队列的单调性,特别适合解决滑动窗口相关的最值问题。本文将深入探讨单调队列的原理、实现,并通过doocs/leetcode中的经典题目来展示其强大威力。
单调队列核心原理
基本概念
单调队列维护的是一个具有单调性的序列,通常用于快速获取滑动窗口中的最大值或最小值。其核心思想是:
- 维护单调性:队列中的元素保持单调递增或递减
- 淘汰策略:新元素加入时,淘汰所有破坏单调性的旧元素
- 窗口管理:及时移除超出窗口范围的元素
算法流程
经典实战:滑动窗口最大值
问题描述
给定一个整数数组 nums 和一个大小为 k 的滑动窗口,窗口从数组的最左侧移动到最右侧。你需要找出每个滑动窗口中的最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
暴力解法 vs 单调队列解法
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力遍历 | O(n*k) | O(1) | 小规模数据 |
| 优先队列 | O(n*logk) | O(k) | 中等规模 |
| 单调队列 | O(n) | O(k) | 大规模数据 |
单调队列实现
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
q = deque() # 存储元素索引,维护递减序列
ans = []
for i, x in enumerate(nums):
# 移除超出窗口的元素
if q and i - q[0] >= k:
q.popleft()
# 维护单调递减性
while q and nums[q[-1]] <= x:
q.pop()
q.append(i)
# 当窗口形成时记录最大值
if i >= k - 1:
ans.append(nums[q[0]])
return ans
执行过程分析
以 nums = [1,3,-1,-3,5,3,6,7], k = 3 为例:
| 步骤 | 当前元素 | 队列状态 | 窗口 | 最大值 |
|---|---|---|---|---|
| 1 | 1 | [0] | [1] | - |
| 2 | 3 | [1] | [1,3] | - |
| 3 | -1 | [1,2] | [1,3,-1] | 3 |
| 4 | -3 | [1,2,3] | [3,-1,-3] | 3 |
| 5 | 5 | [4] | [-1,-3,5] | 5 |
| 6 | 3 | [4,5] | [-3,5,3] | 5 |
| 7 | 6 | [6] | [5,3,6] | 6 |
| 8 | 7 | [7] | [3,6,7] | 7 |
进阶应用:满足不等式的最大值
问题描述
给定按x坐标排序的点数组 points 和整数 k,找出 yi + yj + |xi - xj| 的最大值,其中 |xi - xj| <= k。
数学变换与单调队列应用
将原式进行变换:
yi + yj + |xi - xj|
= yi + yj + (xj - xi) # 因为 xj > xi
= (yi - xi) + (xj + yj)
因此问题转化为:对于每个点 (xj, yj),找到前面满足 xj - xi <= k 的点中 (yi - xi) 的最大值。
单调队列解法
from collections import deque
class Solution:
def findMaxValueOfEquation(self, points: List[List[int]], k: int) -> int:
ans = float('-inf')
q = deque() # 存储(x, y)元组
for x, y in points:
# 移除超出窗口的点
while q and x - q[0][0] > k:
q.popleft()
# 计算当前最大值
if q:
ans = max(ans, x + y + q[0][1] - q[0][0])
# 维护单调性:保持 y-x 递减
while q and y - x >= q[-1][1] - q[-1][0]:
q.pop()
q.append((x, y))
return ans
单调队列的变体与应用场景
常见变体
- 单调递增队列:用于求滑动窗口最小值
- 单调递减队列:用于求滑动窗口最大值
- 双单调队列:同时维护最大值和最小值
典型应用场景
| 应用场景 | 问题特征 | 解决方案 |
|---|---|---|
| 滑动窗口最值 | 固定大小窗口,求最值 | 单调队列 |
| 区间最值查询 | 多次查询区间最值 | 单调队列+预处理 |
| 优化动态规划 | DP状态转移需要区间最值 | 单调队列优化 |
| 实时数据流 | 流数据中维护最近k个元素的最值 | 单调队列 |
性能对比与复杂度分析
时间复杂度对比
空间复杂度分析
| 方法 | 空间复杂度 | 说明 |
|---|---|---|
| 暴力解法 | O(1) | 不需要额外空间 |
| 优先队列 | O(k) | 堆的大小为窗口大小 |
| 单调队列 | O(k) | 队列长度最多为k |
| 线段树 | O(n) | 需要构建线段树 |
实战技巧与注意事项
实现技巧
- 索引存储:通常存储元素索引而非值本身,便于判断是否超出窗口
- 双端操作:使用双端队列支持头部删除和尾部操作
- 提前判断:在添加新元素前先处理窗口边界
常见错误
# 错误示例:先添加再判断窗口
q.append(i)
if i - q[0] >= k: # 可能已经添加了超出窗口的元素
q.popleft()
# 正确做法:先判断窗口再添加
if q and i - q[0] >= k:
q.popleft()
# ...维护单调性...
q.append(i)
边界情况处理
- 空队列处理:操作前检查队列是否为空
- 窗口大小为1:特殊情况需要单独处理
- 重复元素:根据题目要求决定是否保留重复值
扩展应用:单调队列在动态规划中的优化
单调队列不仅可以用于滑动窗口问题,还能优化某些动态规划问题的时间复杂度。
带限制的子序列和
问题:给定整数数组 nums 和整数 k,找出长度不超过 k 的连续子数组的最大和。
def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
n = len(nums)
dp = [0] * n
q = deque()
ans = float('-inf')
for i in range(n):
# 移除超出窗口的索引
while q and i - q[0] > k:
q.popleft()
# 计算当前dp值
dp[i] = nums[i]
if q:
dp[i] = max(dp[i], nums[i] + dp[q[0]])
# 维护单调递减队列
while q and dp[i] >= dp[q[-1]]:
q.pop()
q.append(i)
ans = max(ans, dp[i])
return ans
总结与展望
单调队列作为一种高效的数据结构,在解决滑动窗口和最值问题时表现出色。通过本文的实战分析,我们可以看到:
- 时间复杂度优势:从O(n*k)优化到O(n)
- 空间效率:仅需要O(k)的额外空间
- 应用广泛:不仅限于滑动窗口,还能优化动态规划等问题
在实际应用中,建议:
- 熟练掌握单调队列的基本思想和实现
- 理解不同问题如何转化为单调队列可解的形式
- 注意边界条件的处理和代码的健壮性
通过doocs/leetcode中的丰富题目练习,开发者可以深入掌握这一重要算法技巧,为应对复杂的算法面试和实际开发问题打下坚实基础。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



