和栈类似,队列 (Queue) 也是一种线性表,只不过它的操作方式与栈不同。队列在尾部插入元素,在头部删除元素。
队列的基本操作称为入队 (Enqueue) 和出队 (Dequeue)。 它在表的末端 (称为队尾 rear) 进行插入操作,而在表的前端 (称为队头 front) 进行删除操作。
队列是先进先出的,与栈不同,由于这个特性,队列又被称作先进先出表 (First In First Out, FIFO)。
和栈类似,队列使用链表和数组实现都是合法的,链表实现队列是很显然的,只要各维护好队头和队尾指针即可。无论是入队还是出队,时间复杂度都是 O(1)。
队列的数组实现稍微麻烦,但也不复杂,以下内容讨论的都是数组实现的队列。
队列的数组实现
队列的数组实现严格来说需要维护两个指针,分别指向队头和队尾。队头指针指向队头元素,队尾指针指向队尾元素的下一个位置。这样,队列的大小就是队尾指针减去队头指针。
但是由于 Go Slice 的特性,我们可以不用维护队头指针,只需要维护队尾指针即可。
这是最简单的队列实现,很容易写出代码:
1type Queue struct {
2 data []interface{}
3 tail int
4}
5
6func NewQueue() *Queue {
7 return &Queue{
8 data: make([]interface{}, 0),
9 tail: 0,
10 }
11}
12
13func (q *Queue) Enqueue(v interface{}) {
14 q.data = append(q.data, v)
15 q.tail++
16 return
17}
18
19func (q *Queue) Dequeue() (interface{}, error) {
20 if q.tail == 0 {
21 return errors.New("queue is empty")
22 }
23 v := q.data[0]
24 q.data = q.data[1:]
25 return v, nil
26}
但是对于像 C 那种没那么灵活的语言显然这样是不行的,Go Slice 简化了很多东西,这样实现队列就没有任何难点了,所以还是给出 C 的实现。
队列的数组实现(C)
C 语言中,数组是固定大小的,没有办法像 Go Slice 那样灵活,因此需要更原教旨一点的实现方式。
1typedef struct {
2 int *data;
3 int head;
4 int tail;
5 int size;
6} Queue;
7
8Queue* queueCreate(int maxSize) {
9 Queue *q = (Queue *)malloc(sizeof(Queue));
10 q->data = (int *)malloc(sizeof(int) * maxSize);
11 q->head = 0;
12 q->tail = 0;
13 q->size = maxSize;
14 return q;
15}
16
17bool queueEnQueue(Queue* obj, int value) {
18 if (queueIsFull(obj)) {
19 return false;
20 }
21 obj->data[obj->tail] = value;
22 obj->tail = obj->tail + 1;
23 return true;
24}
25
26bool queueDeQueue(Queue* obj) {
27 if (queueIsEmpty(obj)) {
28 return false;
29 }
30 obj->head = obj->head + 1;
31 return true;
32}
但这样一来有一个潜在的问题,当几次入队出队操作之后,队列看似是满了,但实际上队列的头部还有许多空闲空间,这样就造成了空间的浪费。
一个解决方法是,只要头指针和尾指针到达数组末尾,就重新绕回开头。这种实现称为循环队列。
循环队列使得队列判空和判满操作变得复杂,需要额外的判断。
一种常用的方法是,以一个单位空间为代价,即少用一个元素的空间,这样就可以区分队列满和队列空的情况。
当头尾指针相同时,认为队空,而当尾指针的下一个位置是头指针时,认为队满。
这样一来就有了下面的完整实现:
1typedef struct {
2 int *data;
3 int head;
4 int tail;
5 int size;
6} Queue;
7
8Queue* queueCreate(int maxSize) {
9 Queue *q = (Queue *)malloc(sizeof(Queue));
10 q->data = (int *)malloc(sizeof(int) * maxSize);
11 q->head = 0;
12 q->tail = 0;
13 q->size = maxSize;
14 return q;
15}
16
17bool queueIsEmpty(Queue* obj) {
18 return obj->head == obj->tail;
19}
20
21bool queueIsFull(Queue* obj) {
22 return (obj->tail + 1) % obj->size == obj->head;
23}
24
25bool queueEnQueue(Queue* obj, int value) {
26 if (queueIsFull(obj)) {
27 return false;
28 }
29 obj->data[obj->tail] = value;
30 obj->tail = (obj->tail + 1) % obj->size;
31 return true;
32}
33
34bool queueDeQueue(Queue* obj) {
35 if (queueIsEmpty(obj)) {
36 return false;
37 }
38 obj->head = (obj->head + 1) % obj->size;
39 return true;
40}
41
42int queueFront(Queue* obj) {
43 if (queueIsEmpty(obj)) {
44 return -1;
45 }
46 return obj->data[obj->head];
47}
48
49void queueFree(Queue* obj) {
50 free(obj->data);
51 free(obj);
52}
一些练习
意外的,队列在算法题中的考察并不多。至少 LeetCode 上很少有纯粹使用队列的题目。
但是,队列的应用场景很广,比如 BFS、LRU Cache 等等,都是使用队列来实现的。
操作系统,计算机网络中有许多排队问题,都是需要队列来模拟的。
LeetCode 622. 设计循环队列
这道题目是 LeetCode 上唯一一道纯粹使用队列的题目。
1type MyCircularQueue struct {
2 data []int
3 tail int
4 head int
5 size int
6}
7
8
9func Constructor(k int) MyCircularQueue {
10 return MyCircularQueue{
11 data: make([]int, k + 1),
12 tail: 0,
13 head: 0,
14 size: k + 1,
15 }
16}
17
18
19func (this *MyCircularQueue) EnQueue(value int) bool {
20 if this.IsFull() {
21 return false
22 }
23 this.data[this.tail] = value
24 this.tail = (this.tail + 1) % this.size
25 return true
26}
27
28
29func (this *MyCircularQueue) DeQueue() bool {
30 if this.IsEmpty() {
31 return false
32 }
33 this.head = (this.head + 1) % this.size
34 return true
35}
36
37
38func (this *MyCircularQueue) Front() int {
39 if this.IsEmpty() {
40 return -1
41 }
42 return this.data[this.head]
43}
44
45
46func (this *MyCircularQueue) Rear() int {
47 if this.IsEmpty() {
48 return -1
49 }
50 return this.data[(this.tail - 1 + this.size) % this.size]
51}
52
53
54func (this *MyCircularQueue) IsEmpty() bool {
55 return this.head == this.tail
56}
57
58
59func (this *MyCircularQueue) IsFull() bool {
60 return (this.tail + 1) % this.size == this.head
61}
62
63
64/**
65 * Your MyCircularQueue object will be instantiated and called as such:
66 * obj := Constructor(k);
67 * param_1 := obj.EnQueue(value);
68 * param_2 := obj.DeQueue();
69 * param_3 := obj.Front();
70 * param_4 := obj.Rear();
71 * param_5 := obj.IsEmpty();
72 * param_6 := obj.IsFull();
73 */
结果我还是把正经循环队列用 Go 又写了一遍。
评论