C 语言实现队列深入讲解
2022-05-206.0k 阅读
队列的基本概念
队列(Queue)是一种特殊的线性数据结构,它按照先进先出(First In First Out,FIFO)的原则存储数据,先进入的数据被先读取出来。可以将队列想象成日常生活中的排队场景,比如在银行排队办理业务,先到的人先接受服务,后到的人在队尾依次等待。在计算机科学中,队列常用于处理需要按顺序处理的数据,例如任务调度、打印队列等。
队列的操作
- 入队(Enqueue):将一个新元素添加到队列的末尾。这就好比在排队时,新到来的人站到队伍的最后面。
- 出队(Dequeue):从队列的头部移除一个元素。类似排队时,排在最前面的人办理完业务离开队伍。
- 获取队头元素(Front):查看队列头部的元素,但并不移除它。如同在排队时,我们可以看到排在最前面的人是谁,但他还没有离开队伍。
- 获取队尾元素(Rear):查看队列尾部的元素。类似于看到队伍的最后一个人。
- 判断队列是否为空(IsEmpty):检查队列中是否有元素。如果队列为空,就像排队的队伍没有人一样。
- 获取队列长度(Size):返回队列中元素的个数。等同于队伍中的人数。
C 语言实现队列的方式
在 C 语言中,我们可以使用数组或链表来实现队列。下面我们将分别介绍这两种实现方式。
使用数组实现队列
使用数组实现队列是一种较为简单直观的方法。我们定义一个数组来存储队列中的元素,并使用两个变量来记录队列的头部和尾部位置。
代码示例
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
} Queue;
// 初始化队列
void initQueue(Queue *q) {
q->front = 0;
q->rear = 0;
}
// 判断队列是否为空
int isQueueEmpty(Queue *q) {
return q->front == q->rear;
}
// 判断队列是否已满
int isQueueFull(Queue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
// 入队操作
void enqueue(Queue *q, int value) {
if (isQueueFull(q)) {
printf("队列已满,无法入队\n");
return;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_SIZE;
}
// 出队操作
int dequeue(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法出队\n");
exit(1);
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
return value;
}
// 获取队头元素
int getFront(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法获取队头元素\n");
exit(1);
}
return q->data[q->front];
}
// 获取队尾元素
int getRear(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法获取队尾元素\n");
exit(1);
}
return q->data[(q->rear - 1 + MAX_SIZE) % MAX_SIZE];
}
// 获取队列长度
int getQueueSize(Queue *q) {
return (q->rear - q->front + MAX_SIZE) % MAX_SIZE;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
enqueue(&q, 30);
printf("队列长度: %d\n", getQueueSize(&q));
printf("队头元素: %d\n", getFront(&q));
printf("队尾元素: %d\n", getRear(&q));
printf("出队元素: %d\n", dequeue(&q));
printf("队列长度: %d\n", getQueueSize(&q));
return 0;
}
代码解析
- 结构体定义:我们定义了一个
Queue
结构体,其中包含一个data
数组用于存储队列元素,front
和rear
变量分别表示队列的头部和尾部位置。 - 初始化队列:
initQueue
函数将front
和rear
初始化为 0。 - 判断队列是否为空:
isQueueEmpty
函数通过判断front
和rear
是否相等来确定队列是否为空。 - 判断队列是否已满:
isQueueFull
函数使用取模运算来判断队列是否已满,因为我们使用循环队列来避免数组越界。 - 入队操作:
enqueue
函数将新元素添加到rear
位置,然后更新rear
。如果队列已满,给出提示并返回。 - 出队操作:
dequeue
函数返回front
位置的元素,然后更新front
。如果队列为空,输出错误信息并退出程序。 - 获取队头和队尾元素:
getFront
和getRear
函数分别返回队头和队尾元素,同样在队列为空时给出错误提示。 - 获取队列长度:
getQueueSize
函数通过rear
和front
的差值并结合取模运算来计算队列长度。
使用链表实现队列
使用链表实现队列可以避免数组实现中可能出现的固定大小限制。链表中的每个节点存储一个元素,并且节点之间通过指针连接。
代码示例
#include <stdio.h>
#include <stdlib.h>
typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
typedef struct {
QueueNode *front;
QueueNode *rear;
} Queue;
// 初始化队列
void initQueue(Queue *q) {
q->front = NULL;
q->rear = NULL;
}
// 判断队列是否为空
int isQueueEmpty(Queue *q) {
return q->front == NULL;
}
// 入队操作
void enqueue(Queue *q, int value) {
QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));
newNode->data = value;
newNode->next = NULL;
if (isQueueEmpty(q)) {
q->front = newNode;
q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
}
// 出队操作
int dequeue(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法出队\n");
exit(1);
}
QueueNode *temp = q->front;
int value = temp->data;
q->front = q->front->next;
free(temp);
return value;
}
// 获取队头元素
int getFront(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法获取队头元素\n");
exit(1);
}
return q->front->data;
}
// 获取队尾元素
int getRear(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法获取队尾元素\n");
exit(1);
}
return q->rear->data;
}
// 获取队列长度
int getQueueSize(Queue *q) {
int count = 0;
QueueNode *current = q->front;
while (current != NULL) {
count++;
current = current->next;
}
return count;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
enqueue(&q, 30);
printf("队列长度: %d\n", getQueueSize(&q));
printf("队头元素: %d\n", getFront(&q));
printf("队尾元素: %d\n", getRear(&q));
printf("出队元素: %d\n", dequeue(&q));
printf("队列长度: %d\n", getQueueSize(&q));
return 0;
}
代码解析
- 结构体定义:我们定义了两个结构体,
QueueNode
用于表示链表中的节点,包含数据和指向下一个节点的指针;Queue
结构体包含front
和rear
指针,分别指向队列的头部和尾部节点。 - 初始化队列:
initQueue
函数将front
和rear
初始化为NULL
。 - 判断队列是否为空:
isQueueEmpty
函数通过判断front
是否为NULL
来确定队列是否为空。 - 入队操作:
enqueue
函数创建一个新节点,将其数据设置为传入的值,并将其next
指针设置为NULL
。如果队列为空,将front
和rear
都指向新节点;否则,将新节点添加到rear
节点的后面,并更新rear
。 - 出队操作:
dequeue
函数保存front
节点的数据,然后将front
指针移动到下一个节点,并释放原来的front
节点。如果队列为空,输出错误信息并退出程序。 - 获取队头和队尾元素:
getFront
和getRear
函数分别返回front
和rear
节点的数据,同样在队列为空时给出错误提示。 - 获取队列长度:
getQueueSize
函数通过遍历链表并计数节点的方式来获取队列长度。
循环队列的优化
在使用数组实现队列时,普通的线性队列可能会出现假溢出的问题,即数组的前端有空闲空间,但后端已满,导致无法继续入队。为了解决这个问题,我们可以使用循环队列。
循环队列的原理
循环队列通过将数组视为一个环形结构,使得 rear
指针在到达数组末尾时可以回到数组开头。当 rear
到达数组末尾时,如果 front
前面有空闲空间,rear
可以从数组开头继续存储元素。
循环队列的实现细节
- 初始化:与普通数组队列类似,初始化
front
和rear
为 0。 - 入队:在入队时,计算新的
rear
位置使用取模运算(rear + 1) % MAX_SIZE
。如果新的rear
等于front
,则表示队列已满。 - 出队:出队时,计算新的
front
位置同样使用取模运算(front + 1) % MAX_SIZE
。
代码改进
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int front;
int rear;
} Queue;
// 初始化队列
void initQueue(Queue *q) {
q->front = 0;
q->rear = 0;
}
// 判断队列是否为空
int isQueueEmpty(Queue *q) {
return q->front == q->rear;
}
// 判断队列是否已满
int isQueueFull(Queue *q) {
return (q->rear + 1) % MAX_SIZE == q->front;
}
// 入队操作
void enqueue(Queue *q, int value) {
if (isQueueFull(q)) {
printf("队列已满,无法入队\n");
return;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % MAX_SIZE;
}
// 出队操作
int dequeue(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法出队\n");
exit(1);
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
return value;
}
// 获取队头元素
int getFront(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法获取队头元素\n");
exit(1);
}
return q->data[q->front];
}
// 获取队尾元素
int getRear(Queue *q) {
if (isQueueEmpty(q)) {
printf("队列为空,无法获取队尾元素\n");
exit(1);
}
return q->data[(q->rear - 1 + MAX_SIZE) % MAX_SIZE];
}
// 获取队列长度
int getQueueSize(Queue *q) {
return (q->rear - q->front + MAX_SIZE) % MAX_SIZE;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
enqueue(&q, 30);
printf("队列长度: %d\n", getQueueSize(&q));
printf("队头元素: %d\n", getFront(&q));
printf("队尾元素: %d\n", getRear(&q));
printf("出队元素: %d\n", dequeue(&q));
printf("队列长度: %d\n", getQueueSize(&q));
return 0;
}
通过上述代码,我们可以看到循环队列的实现避免了假溢出问题,提高了数组空间的利用率。
队列在实际应用中的场景
- 任务调度:在操作系统中,任务调度器通常使用队列来管理等待执行的任务。新的任务被添加到队列的末尾,而调度器从队列头部取出任务并执行,确保任务按照提交的顺序依次处理。
- 打印队列:在打印机的工作过程中,打印任务会被放入一个队列中。打印机按照队列的顺序依次打印文档,避免多个任务同时争抢打印机资源,保证打印工作的有序进行。
- 广度优先搜索(BFS):在图论算法中,广度优先搜索算法使用队列来遍历图的节点。从起始节点开始,将其邻居节点依次加入队列,然后从队列中取出节点并继续扩展其邻居,从而实现广度优先的遍历。
- 消息队列:在分布式系统中,消息队列用于在不同的组件之间传递消息。生产者将消息发送到队列,消费者从队列中取出消息进行处理,实现异步通信和解耦系统组件。
队列实现中的常见问题与解决方法
- 内存管理问题(链表实现):在链表实现队列时,需要注意内存的分配和释放。每次入队时要确保正确分配内存给新节点,而出队时要及时释放已移除节点的内存,避免内存泄漏。
- 数组越界问题(数组实现):使用数组实现队列时,要特别注意数组索引的边界检查。循环队列通过取模运算来处理边界,但仍然需要正确判断队列是否已满和为空,防止数组越界访问。
- 性能问题:对于频繁的入队和出队操作,链表实现可能在内存分配和释放上有一定开销,而数组实现可能受限于固定大小。在实际应用中,需要根据具体需求和数据规模选择合适的实现方式,或者对实现进行优化,例如使用动态数组来解决数组固定大小的问题。
通过以上对 C 语言实现队列的深入讲解,包括基本概念、数组和链表两种实现方式、循环队列的优化、实际应用场景以及常见问题解决方法,希望读者对队列这一数据结构在 C 语言中的实现和应用有更全面深入的理解,能够在实际编程中灵活运用队列来解决各种问题。