队列与单调队列基础原理与题目说明
文章目录
- 队列与单调队列基础原理与题目说明
-
- [一、 队列(Queue)与双端队列(Deque)核心机制](#一、 队列(Queue)与双端队列(Deque)核心机制)
-
- [1.1 基础概念](#1.1 基础概念)
- [1.2 核心操作与优势](#1.2 核心操作与优势)
- [二、 核心对比:单调队列 vs 单调栈](#二、 核心对比:单调队列 vs 单调栈)
- [三、 单调队列实战演练](#三、 单调队列实战演练)
-
- [[239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/)](#239. 滑动窗口最大值)
🔗 查看完整专栏(LeetCode基础算法专栏)
点击阅读:Python 数据结构与语法速查笔记
点击阅读:哈希表基础原理与题目说明
点击阅读:双指针基础原理与题目说明
点击阅读:滑动窗口基础原理与题目说明
特别说明:
本文为个人的 LeetCode 刷题与学习笔记,内容仅供学习与交流使用,禁止转载或用于商业用途。需要强调的是,文中的题目解法不一定是最优解(可能存在时间或空间复杂度的进一步优化空间),主要目的是分享个人的解题思路与逻辑实现,仅供参考。 笔记内容为个人理解与总结,可能存在疏漏或偏差,欢迎读者自行甄别并交流探讨。

一、 队列(Queue)与双端队列(Deque)核心机制
1.1 基础概念
- 普通队列(Queue) :遵循 先进先出(FIFO, First In First Out) 原则的线性数据结构。常用于广度优先搜索(BFS)、任务调度等场景。
- 双端队列(Deque) :允许在两端(队首和队尾)同时进行插入和删除操作 的高阶结构。在 Python 中通常使用
collections.deque实现。
1.2 核心操作与优势
在 Python 中,双端队列的左右两端操作时间复杂度均为 O ( 1 ) O(1) O(1):
- 入队 :
append(x)(尾部插入) /appendleft(x)(头部插入) - 出队 :
pop()(尾部弹出) /popleft()(头部弹出)
算法优势:
双端队列能够高效地维护一个区间(窗口)的元素集合。因为可以同时从队首和队尾进行动态调整,它成为了解决滑动窗口极值问题的绝佳容器。
二、 核心对比:单调队列 vs 单调栈
在算法实战中,单调队列 与单调栈常常容易混淆。单调队列本质上是基于双端队列实现的,用于维护区间内元素的单调性(递增或递减)。两者的核心区别如下:
| 特性维度 | 单调队列(Monotonic Queue) | 单调栈(Monotonic Stack) |
|---|---|---|
| 访问范围 | 可在 队首 和 队尾 两端操作 | 仅能在 栈顶 一端操作 |
| 典型用途 | 滑动窗口极值(如窗口最大/最小值) | 全局次序问题(如寻找下一个更大元素、括号匹配) |
| 维护规律 | 队列中元素严格单调递减或递增 | 栈中元素单调递增或递减 |
| 动态维护 | 可以随窗口滑动,从队首主动删除过期元素 | 属于全局累加问题,通常不涉及因"过期"而强制弹出 |
三、 单调队列实战演练
单调队列最经典的实战场景即是"滑动窗口最大值",它将暴力解法的 O ( n ⋅ k ) O(n \cdot k) O(n⋅k) 复杂度完美优化到了 O ( n ) O(n) O(n)。
239. 滑动窗口最大值
题目描述:
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。请返回滑动窗口中的最大值列表。
解题思路:
使用双端队列(deque)维护一个单调递减 的队列,队列中存储的是数组的索引而非具体值(方便判断元素是否滑出窗口)。
- 队首:永远维护当前窗口内最大值的索引。
- 队尾:当新元素准备入队时,将队列中所有对应值小于当前新元素的索引全部弹出(因为当前新元素不仅更大,且生命周期更长,前面的小元素永无出头之日)。
状态流转模拟(单调队列):
以数组 [1, 3, -1, -3, 5, 3, 6, 7],k=3 为例:
| 窗口状态 | 队列内索引 | 队列对应元素 | 状态流转解释 |
|---|---|---|---|
[1, 3, -1] |
[1, 2] |
[3, -1] |
入队 3 时,因 3 > 1,将 1 的索引弹出(1 不可能成为后续最大值) |
[3, -1, -3] |
[1, 2, 3] |
[3, -1, -3] |
入队 -3,因 -3 < -1,符合单调递减,保留,队列长度为 3 |
[-1, -3, 5] |
[4] |
[5] |
入队 5,因 5 大于队尾对应值,队尾全部弹出 ,最后只保留 5 |
[-3, 5, 3] |
[4, 5] |
[5, 3] |
入队 3,因 3 < 5,符合递减,保留入队 |
[5, 3, 6] |
[6] |
[6] |
入队 6,因 6 大于前面的值,队尾全部弹出 |
[3, 6, 7] |
[7] |
[7] |
入队 7,因 7 > 6,队尾弹出,此时队首为 7 |
核心代码:
py
from typing import List
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# 1. 创建双端队列,用于存储元素的索引
deq = deque()
ans = []
# 2. 遍历数组,动态维护单调队列
for i, num in enumerate(nums):
# [Process] 维护单调性:若队尾对应的元素小于当前元素,则弹出队尾
# 因为当前元素更大且更晚过期,被弹出的元素绝对不可能成为最大值
while deq and nums[deq[-1]] < num:
deq.pop()
# [Process] 将当前元素的索引加入队尾
deq.append(i)
# [Process] 维护窗口边界:若队首的索引已经滑出当前窗口(i-k),则弹出队首
if deq[0] <= i - k:
deq.popleft()
# [Process] 记录答案:当窗口形成后(索引 i >= k-1),队首即为当前窗口最大值
if i >= k - 1:
ans.append(nums[deq[0]])
return ans
复杂度分析:
- 时间复杂度 : O ( n ) O(n) O(n)。虽然
for循环内嵌套了while循环,但每个元素的索引最多入队一次、出队一次,总体操作次数不超过 2 n 2n 2n。 - 空间复杂度 : O ( k ) O(k) O(k)。单调队列中最多存储 k k k 个元素的索引。