Queue


Queue(队列)

基本特性

  • FIFO原则:最先添加的元素最先被移除
  • 两个主要操作端
    • 队尾(rear/back):添加元素的位置
    • 队头(front/head):移除元素的位置
  • 约束:只能在两端操作,不能在中间插入或删除

逻辑结构

┌─────────────────────────────────────────┐
│                 Queue                   │
├──────────┬──────┬──────┬──────┬─────────┤
│   Front  │      │      │      │   Rear  │
│  (Head)  │      │      │      │  (Tail) │
├──────────┴──────┴──────┴──────┴─────────┤
│   ^ 出队方向               入队方向 ^   │
│   <──────────────────────────────────── │
└─────────────────────────────────────────┘

状态演变

初始状态(空队列):
┌───┬───┬───┬───┬───┬───┐
│   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┘
^
front = rear = 0

入队元素 A:
┌───┬───┬───┬───┬───┬───┐
│ A │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┘
↑      ^
front  rear

入队元素 B, C:
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │   │   │   │
└───┴───┴───┴───┴───┴───┘
^            ^
front        rear

出队元素 A:
┌───┬───┬───┬───┬───┬───┐
│   │ B │ C │   │   │   │
└───┴───┴───┴───┴───┴───┘
     ^       ^
    front   rear

操作

  • 入队(enqueue)
    • 在队尾添加元素,时间复杂度O(1)
  • 出队(dequeue)
    • 从队头移除并返回元素,时间复杂度O(1)
  • 查看队头元素,不移除(peek)
    • 时间复杂度:O(1)
  • 检查队列是否为空
  • 返回队列中元素数量

实现

基本队列(Naive Queue)

基于数组的实现

这是一种很直观的做法,但是非常不推荐\

实现方式

  • 用数组arr[N]存储元素
  • front指向队头,rear指向队尾
  • 入队:arr[rear++] = x
  • 出队:返回arr[front++]

操作很简单,但是会造成以下问题

  • 空间浪费
    • 出队操作后,front会不断右移
    • 数组前面的空间空了,但无法复用
    • 例如:容量10,入队10个,出队5个,数组前半部分空了,但rear还在10 -> 后续最多只能再放0个元素
  • 低效的解决方式
    • 可以每次出队后,把数组前半部分的元素整体向左移动
    • 代价高:O(n)每次出队 -> 很慢

这种做法仅适用于理解队列状态变化
工业级基于数组的实现是Circular Queue

基于链表的实现

这是正统的做法

特点

  • enqueue:O(1)
  • dequeue:O(1)
  • 不需要扩容
head -> [A] -> [B] -> [C] -> null

双端队列(Deque)

Deque(Double Ended Queue),可以在队头和队尾进行插入和删除操作的数据结构

操作含义时间复杂度
push_front从头部插入O(1)
push_back从尾部插入O(1)
pop_front从头部删除O(1)
pop_back从尾部删除O(1)

逻辑结构

┌─────────────────────────────────────────┐
│               Deque                     │
├───────┬─────────┬─────────┬─────────────┤
│ Front │         │         │     Rear    │
│ (Head)│         │         │    (Tail)   │
├───────┴─────────┴─────────┴─────────────┤
│  ← 出/入队方向        出/入队方向 →     │
└─────────────────────────────────────────┘

状态演变

初始状态:
┌───┬───┬───┬───┬───┐
│   │   │   │   │   │
└───┴───┴───┴───┴───┘

从前端添加 X,从后端添加 Y:
┌───┬───┬───┬───┬───┐
│ X │   │   │   │ Y │
└───┴───┴───┴───┴───┘
 ↑               ↑
front          rear

继续从前端添加 A,从后端添加 B:
┌───┬───┬───┬───┬───┐
│ A │ X │   │ Y │ B │
└───┴───┴───┴───┴───┘
 ↑           ↑
front      rear

实现

双向链表实现
结构
node <-> node <-> node
优点
  • 实现简单
  • 任意长度
  • 插入删除绝对O(1)
缺点
  • 每个节点两个指针 -> 内存大
  • cache miss 非常严重
  • 实际性能差

高性能场景不会使用这个版本

循环数组

这是最常见的手写实现

核心思想

用一个数组 + 两个索引

[ _ _ A B C D _ _ ]
      ↑       ↑
    head    tail

索引取模

next = (i + 1) % capacity;
结构
buffer[]
head index
tail index
size
优点
  • 连续内存
  • cache 极友好
  • 实现简单
  • 实际工程最常用
缺点
  • 需要扩容
  • 扩容需要整体搬移
STL风格分段数组

这是std::deque的真实实现方式

结构类似这样

[block][block][block][block]
   ↓      ↓
 [64]   [64]

本质是指针数组 -> 每个指针指向一个小块连续内存

优点
  • 两端扩容不用整体搬迁
  • 连续块 + 灵活
  • 综合性能最强
缺点
  • 实现复杂
  • 手写成本高

使用场景

Deque的接口简单,底层实现高度工程化
适合用于两端高频操作的场景
Deque是系统级最重要的数据结构之一
简单程度像队列,工程价值像内存管理器
算法领域Deque是万金油

不该使用Deque的场景

  • 只尾部push/pop -> vector
  • 只FIFO -> queue
  • 需要随机访问 -> vector
  • 任意位置插删 -> list
  • 按优先级 -> priority_queue

一旦开始用Deque的中间索引访问元素,说明选错结构了

优先队列(Priority Queue)

循环队列(Circular Queue)

阻塞队列(Blocking Queue)

应用场景

  1. BFS
  2. 生产者/消费者模型
  3. 任务调度/消息系统
  4. 单调队列