🚶 队列 — 先来先服务的秩序

队列是BFS的引擎,单调队列是滑动窗口的最优解

📖 队列的核心概念

队列(Queue)是一种先进先出(FIFO)的数据结构,只能从队尾入队、队首出队。队列是 BFS(广度优先搜索)的天然搭档,也是任务调度、消息传递等场景的基础。面试中,队列的进阶应用——单调队列,是滑动窗口最大值等问题的最优解。

队列的模型: 先进先出 (FIFO) 出队 ← [1, 2, 3, 4, 5] ← 入队 队首 队尾 操作: 时间复杂度 enqueue(x) 入队 O(1) dequeue() 出队 O(1) front() 看队首 O(1) empty() 判空 O(1) 队列的进阶形态: ┌──────────────────┬──────────────────────────┐ │ 普通队列 │ BFS、任务调度 │ │ 双端队列 deque │ 两端都可以入出 │ │ 单调队列 │ 维护窗口内最值, O(n)整体 │ │ 优先队列(堆) │ 按优先级出队 │ │ 循环队列 │ 固定大小,空间复用 │ └──────────────────┴──────────────────────────┘

1. 单调队列详解

什么是单调队列?
双端队列中的元素保持单调递增(或递减)。
新元素入队时,从队尾弹出所有不满足单调性的元素。
队首始终是当前窗口的最值。
整体复杂度 O(n):每个元素最多入队出队各一次。
单调递减队列 → 队首是最大值
单调递增队列 → 队首是最小值
窗口滑动时,如果队首元素离开窗口,从队首弹出。

2. Python 队列操作速查

# collections.deque(推荐,O(1)两端操作)
from collections import deque
q = deque()
q.append(1)        # 右端入队
q.appendleft(2)    # 左端入队
q.popleft()        # 左端出队
q.pop()            # 右端出队
q[0]               # 看队首
len(q)             # 长度

# queue.Queue(线程安全,多线程用)
from queue import Queue
q = Queue()
q.put(1)
q.get()

🎯 题目一:滑动窗口最大值 (LC 239)

题目描述:给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到最右侧。你只可以看到在滑动窗口内的 k 个元素。请返回滑动窗口中的最大值。

示例: nums = [1,3,-1,-3,5,3,6,7], k = 3 窗口 单调递减队列(存索引) 最大值 [1,3,-1] [1,2] → nums[1]=3 3 [3,-1,-3] [1,2,3]→ nums[1]=3 3 [-1,-3,5] [4] → nums[4]=5 5 [-3,5,3] [4,5] → nums[4]=5 5 [5,3,6] [6] → nums[6]=6 6 [3,6,7] [7] → nums[7]=7 7 队列操作过程: i=0(1): 入队 → [0] i=1(3): 1<3,弹0 → [1] i=2(-1): 入队 → [1,2] → 窗口[0,2], max=3 i=3(-3): 入队 → [1,2,3] → 窗口[1,3], max=3 i=4(5): -3,-1,3都<5,全弹 → [4] → 窗口[2,4], max=5 i=5(3): 入队 → [4,5] → 窗口[3,5], max=5 i=6(6): 3,5都<6,全弹 → [6] → 窗口[4,6], max=6 i=7(7): 6<7,弹6 → [7] → 窗口[5,7], max=7

思路分析

单调递减队列:
1. 队列存索引(方便判断是否出窗口),对应值单调递减
2. 新元素入队:从队尾弹出所有 ≤ 新元素的元素(它们不可能成为最大值)
3. 队首出队:如果队首索引 ≤ i - k,说明已出窗口
4. 队首就是当前窗口最大值
时间复杂度:O(n),空间复杂度:O(k)

代码实现

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        from collections import deque
        dq = deque()  # 存索引,对应值单调递减
        result = []
        
        for i, num in enumerate(nums):
            # 移除超出窗口的队首
            while dq and dq[0] <= i - k:
                dq.popleft()
            # 维护单调递减:弹出队尾所有 ≤ 当前值的
            while dq and nums[dq[-1]] <= num:
                dq.pop()
            dq.append(i)
            # 窗口形成后开始记录答案
            if i >= k - 1:
                result.append(nums[dq[0]])
        
        return result

复杂度分析

时间复杂度:O(n) — 每个元素最多入队出队各一次

空间复杂度:O(k) — 队列最多 k 个元素

为什么是 O(n)?虽然内层有 while 循环,但每个元素最多被弹出一次。总操作次数 ≤ 2n,均摊每次 O(1)。

变体练习

🎯 题目二:用栈实现队列 (LC 225)

题目描述:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

双队列实现栈: 方法: push时倒腾 queue1: [1, 2, 3] queue2: [] push(4): 1. 入queue2: queue2 = [4] 2. queue1全部倒入queue2: queue2 = [4, 1, 2, 3] 3. 交换queue1和queue2: queue1 = [4, 1, 2, 3] pop(): queue1出队 = 4 ← 后入先出! 方法2: push O(1), pop O(n) push直入queue1, pop时把前n-1个倒入queue2

思路分析

方法一:push 时倒腾(push O(n), pop O(1)):
1. 新元素先入空的 queue2
2. 将 queue1 全部倒入 queue2
3. 交换 queue1 和 queue2
这样 queue1 的队首始终是最后入栈的元素。
方法二:pop 时倒腾(push O(1), pop O(n)):
push 直接入 queue1。
pop 时将 queue1 前 n-1 个倒入 queue2,最后一个就是栈顶。
交换 queue1 和 queue2。

代码实现(方法一)

class MyStack:
    def __init__(self):
        from collections import deque
        self.q1 = deque()
        self.q2 = deque()

    def push(self, x: int) -> None:
        self.q2.append(x)
        while self.q1:
            self.q2.append(self.q1.popleft())
        self.q1, self.q2 = self.q2, self.q1

    def pop(self) -> int:
        return self.q1.popleft()

    def top(self) -> int:
        return self.q1[0]

    def empty(self) -> bool:
        return not self.q1

复杂度分析

push:O(n) | pop/top/empty:O(1)

空间复杂度:O(n)

变体练习

🎯 题目三:设计循环队列 (LC 622)

题目描述:设计你的循环队列实现。循环队列是一种线性数据结构,其操作表现基于 FIFO 原则并且队尾被连接在队首之后以形成一个循环,利用固定大小的数组实现。

循环队列结构 (capacity = 5): front ↓ [_, A, B, C, _, _] ↑ rear 关键: 用 front 和 rear 指针 判空: front == rear 判满: (rear + 1) % capacity == front 牺牲一个空间来区分空和满 入队: arr[rear] = val; rear = (rear+1) % k 出队: front = (front+1) % k 取模实现循环!

思路分析

数组 + 双指针:
用大小为 k+1 的数组,front 和 rear 指针。
判空:front == rear
判满:(rear + 1) % capacity == front(牺牲一个位置)
入队:arr[rear] = val, rear = (rear+1) % capacity
出队:front = (front+1) % capacity
时间复杂度:所有操作 O(1)

代码实现

class MyCircularQueue:
    def __init__(self, k: int):
        self.capacity = k + 1
        self.arr = [0] * self.capacity
        self.front = 0
        self.rear = 0

    def enQueue(self, value: int) -> bool:
        if self.isFull():
            return False
        self.arr[self.rear] = value
        self.rear = (self.rear + 1) % self.capacity
        return True

    def deQueue(self) -> bool:
        if self.isEmpty():
            return False
        self.front = (self.front + 1) % self.capacity
        return True

    def Front(self) -> int:
        if self.isEmpty():
            return -1
        return self.arr[self.front]

    def Rear(self) -> int:
        if self.isEmpty():
            return -1
        return self.arr[(self.rear - 1) % self.capacity]

    def isEmpty(self) -> bool:
        return self.front == self.rear

    def isFull(self) -> bool:
        return (self.rear + 1) % self.capacity == self.front

复杂度分析

时间复杂度:所有操作 O(1)

空间复杂度:O(k)

也可以用 count 计数器代替"牺牲一个位置"的技巧:count == 0 判空,count == capacity 判满。两种方式都行,面试时说清楚即可。

变体练习

成就解锁:队列指挥官 — 掌握单调队列、双队列实现栈、循环队列三大经典应用
LeetCode AC验证:LC 239 Sliding Window Maximum ✅ | LC 225 Implement Stack using Queues ✅ | LC 622 Design Circular Queue ✅

📝 课后练习

  1. LC 346 数据流中的移动平均值(固定大小队列)
  2. LC 862 和至少为 K 的最短子数组(前缀和+单调队列,Hard)
  3. LC 1438 绝对差不超过限制的最长连续子数组
  4. LC 641 设计循环双端队列
  5. LC 933 最近的请求次数(队列模拟时间窗口)

🔑 本课要点回顾

📚 扩展阅读

思考题:如果要求滑动窗口的最大值和最小值同时维护(如 LC 1438),如何设计?提示:同时维护两个单调队列,一个递减找 max,一个递增找 min。