排队是生活中一个十分典型的队列,先到的人可以到达队列的队首,后到的则跟在后面成为队尾

比如说,在银行中,柜台会处理位于队首的客户的业务。完成后,队列的下一个客户会到达柜台办理。

这种先进先出 (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();

// 元素入队 (Enqueue) -> 队尾
queue.Enqueue(1);
queue.Enqueue(4);
queue.Enqueue(5);

// 获取队首元素 (Peek) -> 不移除
// queue: 1 (Front), 4, 5 (Rear)
int peek = queue.Peek(); // 返回 1

// 元素出队 (Dequeue) -> 移除队首
// queue: 4, 5
int dequeue = queue.Dequeue(); // 返回 1

// 尝试出队 (TryDequeue) - .NET Core 2.0+
if (queue.TryDequeue(out int result))
{
Console.WriteLine(result); // 4
}

// 获取队列的长度
int count = queue.Count;

// 判断队列是否为空
bool isEmpty = queue.Count == 0;

实现原理

与栈类似,队列也可以通过数组、链表或列表实现。

.NET是基于数组实现的循环队列
.NET是基于数组实现的循环队列

基于列表 (数组)

简单的数组实现面临一个问题:如果队首出队,所有后续元素都要前移,复杂度为 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; // 记录队首索引

// 入队:O(1)
public void Enqueue(T element) => _queue.Add(element);

// 实际有效长度
public int Count => _queue.Count - _frontIndex;
public bool IsEmpty => Count == 0;

// 访问队首:O(1)
public T Peek()
{
if (IsEmpty) throw new InvalidOperationException("Queue is empty");
return _queue[_frontIndex];
}

// 出队:O(1)
// 仅移动指针,不实际删除列表元素,避免 O(n) 的数据移动
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
_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 的类。

应用场景

  1. 任务调度 / 消息队列
    操作系统、Web服务器处理请求(如 Nginx 请求队列、RabbitMQ)。请求按照到达顺序依次处理。

  2. 广度优先搜索 (BFS)
    图算法中,使用队列来存储待访问的节点,从而实现层层推进的遍历。

  3. 打印机任务
    先提交的文档先打印。