排队是生活中一个十分典型的队列,先到的人可以到达队列的队首,后到的则跟在后面成为队尾。
比如说,在银行中,柜台会处理位于队首的客户的业务。完成后,队列的下一个客户会到达柜台办理。
这种先进先出 (First-In, First-Out, FIFO) 的模式,就是队列(Queue)的核心特征。
- 入队 (Enqueue):将新元素添加到队尾。
- 出队 (Dequeue):移除并返回队首元素。
常用操作
不同于栈(LIFO),队列(FIFO)维持了元素的时序。
元素入队、出队和访问队首元素的时间复杂度在标准实现下均为 O(1)。
在 C# 中,.NET 提供了泛型类 System.Collections.Generic.Queue<T>(注意:需区别于非泛型的 System.Collections.Queue):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| Queue<int> queue = new();
queue.Enqueue(1); queue.Enqueue(4); queue.Enqueue(5);
int peek = queue.Peek();
int dequeue = queue.Dequeue();
if (queue.TryDequeue(out int result)) { Console.WriteLine(result); }
int count = queue.Count;
bool isEmpty = queue.Count == 0;
|
实现原理
与栈类似,队列也可以通过数组、链表或列表实现。
基于列表 (数组)
简单的数组实现面临一个问题:如果队首出队,所有后续元素都要前移,复杂度为 O(n)。为了优化,我们引入 FrontIndex 指针,只移动指针而不移动数据。
但这又导致了空间浪费(队首前面的空间变得不可用)。因此,更成熟的实现(如 .NET 的 Queue<T>)采用循环数组 (Circular Buffer):当Rear指针到达数组末尾时,绕回数组开头继续存放数据(前提是开头有空闲空间)。
以下是一个简化版的、非循环的 List 实现(存在空间浪费问题):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class ListQueue<T> { List<T> _queue = []; private int _frontIndex = 0;
public void Enqueue(T element) => _queue.Add(element); public int Count => _queue.Count - _frontIndex; public bool IsEmpty => Count == 0;
public T Peek() { if (IsEmpty) throw new InvalidOperationException("Queue is empty"); return _queue[_frontIndex]; }
public T Dequeue() { var element = Peek(); _frontIndex++; return element; } }
|
基于链表
链表天然适合实现队列。我们将链表头节点 (Head) 作为队首,尾节点 (Tail) 作为队尾。
- 出队:移除 Head (O(1))
- 入队:在 Tail 后追加新节点 (O(1))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class Node<T>(T item) { public T Item { get; set; } = item; public Node<T>? Next { get; set; } }
class LinkedQueue<T> { private Node<T>? _front; private Node<T>? _rear; private int _count = 0; public int Count => _count;
public void Enqueue(T element) { Node<T> node = new(element); if (_rear == null) { _front = node; _rear = node; } else { _rear.Next = node; _rear = node; } _count++; }
public T Dequeue() { if (_front == null) throw new InvalidOperationException("Queue is empty"); T element = _front.Item; _front = _front.Next; if (_front == null) _rear = null; _count--; return element; }
public T Peek() { if (_front == null) throw new InvalidOperationException("Queue is empty"); return _front.Item; } }
|
对比
| 特性 | 基于循环数组 (标准 Queue<T>) | 基于链表 |
| 内存效率 | 高 (需处理循环逻辑) | 低 (节点额外引用开销) |
| 性能 | 极高 (缓存友好,无GC压力) | 稳定 (无扩容) |
| 扩容 | 满时需要扩容 (O(n)) | 无需扩容 (O(1)) |
双端队列 (Deque)
普通队列只允许一端进、一端出。双端队列 (Double-Ended Queue) 允许在两端都可以进行入队和出队操作。
虽然 LinkedList<T> 可以作为双端队列使用,但 .NET 6 引入了专门的 PriorityQueue(优先级队列),如果你需要双端队列行为,通常只能用 LinkedList<T> 模拟,或者自己通过循环数组实现。
注:Python 等语言有名为 deque 的标准库,C# 暂无直接名为 Deque 的类。
应用场景
任务调度 / 消息队列:
操作系统、Web服务器处理请求(如 Nginx 请求队列、RabbitMQ)。请求按照到达顺序依次处理。
广度优先搜索 (BFS):
图算法中,使用队列来存储待访问的节点,从而实现层层推进的遍历。
打印机任务:
先提交的文档先打印。
本文和右下角标有www.hello-algo.com标识的图片采用 署名-非商业性使用-相同方式共享 4.0 国际 许可协议,转载请注明出处。