跳至主要內容

队列(Queue)

西风逍遥游大约 5 分钟

队列(Queue)

队列 是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。和 一样,队列是一种操作受限制的线性表,但限制方式恰好相反。

队列最核心的性质是先进先出(FIFO,First In First Out):最早放进去的元素,最先被取出来。可以把它想象成排队买票——先来的站在队头,先拿到票;后来的人只能排到队尾,不能插队。

队列的操作

队列的操作同样简单,核心也是两种:从队尾加入,从队头取出。

操作含义
Enqueue / Push入队,在队尾插入一个元素
Dequeue / Pop出队,从队头取出并移除一个元素
Front / Peek查看队头元素,但不移除
isEmpty判断队列是否为空
Size返回队列中元素个数

在理想实现下,入队和出队的时间复杂度都是 O(1)。对空队列执行 DequeuePeek 会导致下溢(Underflow);若容量固定且队满仍入队,则会发生上溢(Overflow)

队列的实现

用链表实现

链表 实现时,通常维护 head(队头)和 tail(队尾)两个指针:入队在 tail 后追加节点,出队删除 head 并前移。只要 tail 指针存在,两端操作都是 O(1)

struct Node { int val; Node* next; };

class Queue {
    Node* head = nullptr;
    Node* tail = nullptr;
public:
    void enqueue(int x) {
        Node* n = new Node{x, nullptr};
        if (tail) tail->next = n;
        else head = n;
        tail = n;
    }
    int dequeue() {
        int v = head->val;
        Node* old = head;
        head = head->next;
        if (!head) tail = nullptr;
        delete old;
        return v;
    }
    bool empty() { return head == nullptr; }
};

链表实现的优点是容量不受限、入队无需搬移元素;缺点是每个节点多一个指针,内存不连续。

用数组实现(循环队列)

数组 实现时,如果每次出队都把后面元素整体前移,复杂度会变成 O(n)。工程上更常用循环队列(Circular Queue):把数组首尾相接,用 frontrear 两个下标分别指向队头、队尾下一个空位,入队出队只移动下标,不搬移数据。

       front          rear
         v              v
  [ | | A | B | C | | | ]
    ^                 ^
    数组末尾绕回开头

rear 走到数组末尾时,下一个位置是下标 0,而不是整体搬移。判断队满、队空时,需要约定是“牺牲一个槽位”还是单独维护 size 计数——两种做法都很常见。

数组实现的优点是缓存友好、常数小;缺点是需要处理循环下标和容量边界,或者像栈一样在写满时扩容。

队列的应用

队列和栈一样看似简单,却无处不在:

  • 广度优先遍历(BFS) 的层序遍历、最短路径的层次扩展,都依赖队列“先处理早进来的节点”这一特性。
  • 任务调度:操作系统把等待 CPU 的进程放进就绪队列,按到达顺序或优先级依次调度;打印队列、消息队列也是同一思路。
  • 缓冲区与流水线:生产者往队尾放数据,消费者从队头取数据,用队列解耦“产生速度”和“处理速度”的不匹配(如键盘输入缓冲、网络包缓冲)。
  • 异步与事件处理:GUI 里用户点击、定时器到期等事件,常被放进事件队列,主线程按顺序逐个处理。

广度优先遍历时队列的变化

广度优先遍历(BFS),本质就是一个不断入队、出队的过程。它的思路是“一层一层向外扩”:先把起点的邻居都访问完,再访问下一层。

以遍历一棵树为例,用队列实现的过程大致是:

  1. 把根节点入队。
  2. 从队头取出一个节点,访问它。
  3. 把它尚未访问的子节点按从左到右顺序入队
  4. 重复第 2、3 步,直到队列为空。

因为先进入队列的节点会先被处理,所以访问顺序天然按层展开——这正是广度优先。

还是这棵树:

        A
       / \
      B   C
     / \
    D   E

队列的变化过程如下:

步骤动作队列内容(队头在左)已访问
1入队 A[A]
2出队 A,入队 B、C[B, C]A
3出队 B,入队 D、E[C, D, E]A B
4出队 C[D, E]A B C
5出队 D[E]A B C D
6出队 E[]A B C D E

最终访问顺序是 A B C D E,正是层序遍历的结果。

对比 里的深度优先示例(A B D E C),同一个数据结构、同一种遍历目标,仅仅因为用了栈还是队列,访问顺序就完全不同——栈负责“往深处钻”,队列负责“按层铺开”

在无权图的最短路问题里,BFS 还有一个重要性质:第一次到达某个顶点时,所走步数一定是最少的。Dijkstra 在边权为 1 的特殊情况下,也可以看作带层次的 BFS 扩展。

双端队列与优先队列

在普通 FIFO 队列之上,还有两个常见变体:

结构特点典型用途
双端队列(Deque)队头、队尾都可以入队/出队滑动窗口、单调队列优化
优先队列(Priority Queue)每次取出优先级最高(或最低)的元素,而非最早入队的任务调度、Dijkstra 堆优化、事件模拟

优先队列的底层往往用堆实现,时间复杂度与纯 FIFO 队列不同(入队/出队通常为 O(log n)),但“按某种规则取下一个该处理的元素”这一抽象,在算法和系统里极其常用。图论中最短路一节提到的堆优化 Dijkstra,用的就是优先队列而非普通队列。

小结

队列用先进先出约束了元素的进出顺序:一端进、一端出,所有元素按到达顺序排队。掌握 enqueue / dequeue、循环数组与链表两种实现,以及 BFS 与任务调度等典型场景,就能理解大多数系统里“排队等待处理”的逻辑。与栈配对记忆效果最好:栈是 LIFO,队列是 FIFO;DFS 用栈,BFS 用队列