队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,在表的后端(rear)进行插入操作。与堆栈一样,队列也是一种操作受限制的线性表,进行插入操作的端称为队尾,进行删除操作的端称为队头。数据的入队和出队遵循先进先出(FIFO, First-In First-Out)的原则。
这篇文章主要介绍通过链表实现队列,但会首先介绍一下队列的几种实现方式和各自的特点、适用场景。
队列的实现方式
在 C 语言中,FIFO 队列的实现方式有以下几种:
数组实现:使用一个固定大小的数组作为队列的存储空间,使用两个指针 front 和 rear 分别指向队列的头部和尾部,通过不断移动指针和调整数组元素的位置来实现入队和出队操作。
链表实现:使用链表作为队列的存储结构,通过定义一个链表节点结构体来存储队列元素的值和指向下一个节点的指针。通过调整链表节点的指针关系来实现入队和出队操作。
循环队列实现:使用一个固定大小的数组作为队列的存储空间,同时使用两个指针 front 和 rear 分别指向队列的头部和尾部。当 rear 指针到达数组末尾时,再次从数组头部开始存储元素,实现循环利用数组空间的效果。
不同实现方式适用的场景
数组实现队列、链表实现队列和循环队列实现,都有各自的特点和适用场景,以下是不同实现方式的一些适用场景:
数组实现适用于以下场景:
队列大小固定,不需要频繁进行大小调整。
元素个数相对较少,不会造成数组空间的浪费。
需要快速随机访问队列元素。
链表实现适用于以下场景:
队列大小不确定,会频繁进行大小调整。
元素个数可能非常大,链表能够动态分配内存。
需要频繁进行插入和删除操作。
循环队列实现适用于以下场景:
队列大小固定,不需要频繁进行大小调整。
队列元素个数可能会超过数组大小,但是可以接受覆盖旧元素的方式。
需要快速入队和出队操作。
根据实际需求和对性能的要求,可以选择适合的实现方式。例如,如果队列大小固定且元素个数不会超过数组大小,可以选择数组实现;如果队列大小不确定且需要频繁进行插入和删除操作,可以选择链表实现;如果队列大小固定但元素个数可能超过数组大小且需要快速入队和出队操作,可以选择循环队列实现。
链表实现队列
定义基于链表的队列结构
为了体现队列中数据的复杂性 ,这里不再使用基本数据类型(如整形、字符型)作为队列中的数据,而是使用了二叉树结构,即在队列中保存的数据是一个二叉树节点,节点中包含节点值、节点的左子树指针和节点的右子树指针。
首先,定义二叉树的结构:
1 2 3 4 5 6 typedef struct tagTreeNode { int val; struct tagTreeNode *left ; struct tagTreeNode *right ; } TreeNode_t;
然后,定义链表的结构:
1 2 3 4 5 typedef struct tagLinkedNode { TreeNode_t *data; struct tagLinkedNode *next ; } LinkedNode_t;
在这里,链表中的成员就是:用于存储二叉树节点数据的结构体和指向下一个链表节点的指针。
最后,定义队列的结构:
1 2 3 4 5 6 7 typedef struct tagQueue { int size; int max; LinkedNode_t *front; LinkedNode_t *rear; } Queue_t;
在这里,队列中的成员就是:队头指针和队尾指针,以及一个记录队列中数据数量的成员变量和一个标记队列可以容纳的最大数据量的成员变量。
队列的创建与初始化
首先,创建一个空队列(队列中还没有存储数据),并初始化成员变量。
1 2 3 4 5 6 7 8 9 Queue_t *createQueue (int max_size) { Queue_t *queue = (Queue_t *)malloc (sizeof (Queue_t)); queue ->front = NULL ; queue ->rear = NULL ; queue ->size = 0 ; queue ->max = max_size; return queue ; }
然后,对于空队列,后续入队操作会有数据存储到队列中,数据需要必要的存储空间来存储。在这里,我们创建并初始化一段空间用于存储数据。
1 2 3 4 5 6 7 LinkedNode_t *createNode (TreeNode_t *data) { LinkedNode_t *newNode = (LinkedNode_t *)malloc (sizeof (LinkedNode_t)); newNode->data = data; newNode->next = NULL ; return newNode; }
判断队列是否为空
1 2 3 4 bool isEmpty (Queue_t *queue ) { return queue ->size == 0 ; }
当然,也可以使用 (!queue->front) && (queue->front == queue->rear)
判断队列是否为空。
判断队列是否已满
1 2 3 4 bool isFull (Queue_t *queue ) { return queue ->size >= queue ->max; }
入队操作
入队操作,即在队列的尾部插入一个数据。
如果队列已满,则无法继续执行入队操作;
如果队列为空,则队列的头指针(front)和尾指针(rear)都指向新插入的数据的存放地址;
否则,链接并更新队列的尾指针(rear)为新插入的数据的存放地址。
入队操作后需要更新队列的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void enQueue (Queue_t *queue , TreeNode_t *data) { if (isFull(queue )) { printf ("queue is full.\n" ); } else { LinkedNode_t *newNode = createNode(data); if (isEmpty(queue )) { queue ->front = queue ->rear = newNode; } else { queue ->rear->next = newNode; queue ->rear = newNode; } queue ->size += 1 ; } }
出队操作
出队操作,即在队列的头部删除一个数据。出队操作需要依次执行以下操作:
获取头指针地址 & 数据;
更新队列头指针;
更新队列大小;
释放原头指针内存空间;
返回数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 TreeNode_t *deQueue (Queue_t* queue ) { if (isEmpty(queue )) { return NULL ; } LinkedNode_t *node = queue ->front; TreeNode_t *data = node->data; if (queue ->size > 1 ) { queue ->front = queue ->front->next; } else { queue ->front = queue ->rear = NULL ; } queue ->size -= 1 ; free (node); return data; }
Tips:出队操作在在更新队列头指针时,需要根据当前队列中节点的数量,决定是否同步更新队列的尾指针。
释放 node
内存空间后,不会造成 data
数据无法访问吗?
不会。这是因为,在释放节点之前,我们先将数据指针 node->data
保存到一个新的变量 data
中,然后释放节点,最后返回保存的数据指针。这样可以确保返回的数据指针依然有效。
打印队列数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void printQueue (Queue_t* queue ) { if (isEmpty(queue )) { printf ("Queue is empty\n" ); } else { printf ("Queue: " ); LinkedNode_t *node = queue ->front; while (node != NULL ) { printf ("%d " , node->data->val); node = node->next; } printf ("\n" ); } }
释放队列
1 2 3 4 5 6 7 8 9 10 11 12 void freeQueue (Queue_t* queue ) { if (queue ) { LinkedNode_t *node = queue ->front; while (node) { LinkedNode_t *temp = node; node = node->next; free (node); } free (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 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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 #include <stdio.h> #include <stdlib.h> #include <stdbool.h> typedef struct tagTreeNode { int val; struct tagTreeNode *left ; struct tagTreeNode *right ; } TreeNode_t; typedef struct tagLinkedNode { TreeNode_t *data; struct tagLinkedNode *next ; } LinkedNode_t; typedef struct tagQueue { int size; int max; LinkedNode_t *front; LinkedNode_t *rear; } Queue_t; Queue_t *createQueue (int max_size) { Queue_t *queue = (Queue_t *)malloc (sizeof (Queue_t)); queue ->front = NULL ; queue ->rear = NULL ; queue ->size = 0 ; queue ->max = max_size; return queue ; } LinkedNode_t *createNode (TreeNode_t *data) { LinkedNode_t *newNode = (LinkedNode_t *)malloc (sizeof (LinkedNode_t)); newNode->data = data; newNode->next = NULL ; return newNode; } bool isEmpty (Queue_t *queue ) { return queue ->size == 0 ; } bool isFull (Queue_t *queue ) { return queue ->size >= queue ->max; } void enQueue (Queue_t *queue , TreeNode_t *data) { if (isFull(queue )) { printf ("queue is full.\n" ); } else { LinkedNode_t *newNode = createNode(data); if (isEmpty(queue )) { queue ->front = queue ->rear = newNode; } else { queue ->rear->next = newNode; queue ->rear = newNode; } queue ->size += 1 ; } } TreeNode_t *deQueue (Queue_t* queue ) { if (isEmpty(queue )) { return NULL ; } LinkedNode_t *node = queue ->front; TreeNode_t *data = node->data; if (queue ->size > 1 ) { queue ->front = queue ->front->next; } else { queue ->front = queue ->rear = NULL ; } queue ->size -= 1 ; free (node); return data; } void printQueue (Queue_t* queue ) { if (isEmpty(queue )) { printf ("Queue is empty\n" ); } else { printf ("Queue: " ); LinkedNode_t *node = queue ->front; while (node != NULL ) { printf ("%d " , node->data->val); node = node->next; } printf ("\n" ); } } void freeQueue (Queue_t* queue ) { if (queue ) { LinkedNode_t *node = queue ->front; while (node) { LinkedNode_t *temp = node; node = node->next; free (node); } free (queue ); } } int main (int argc, char *argv[]) { Queue_t *queue = createQueue(3 ); TreeNode_t *node = (TreeNode_t *)malloc (4 * sizeof (TreeNode_t)); for (int i = 0 ; i < 4 ; ++i) { node[i].val = 10 + i; node[i].left = NULL ; node[i].right = NULL ; } printQueue(queue ); enQueue(queue , &node[0 ]); enQueue(queue , &node[1 ]); enQueue(queue , &node[2 ]); printQueue(queue ); enQueue(queue , &node[3 ]); printQueue(queue ); TreeNode_t *data1 = deQueue(queue ); TreeNode_t *data2 = deQueue(queue ); TreeNode_t *data3 = deQueue(queue ); printf ("Dequeued data: %d %d %d\n" , data1->val, data2->val, data3->val); printQueue(queue ); freeQueue(queue ); free (node); return 0 ; }
程序的输出结果为:
1 2 3 4 5 6 Queue is empty Queue: 10 11 12 queue is full. Queue: 10 11 12 Dequeued data: 10 11 12 Queue is empty