c语言实现队列【由浅入深-数据结构】
文章目录
- 前言
- C语言实现队列的详细知识
- 一、队列的基本概念
- 队列的特性
- 二、队列的实现方式
- 1. 基于链表的实现(常用)
- 2. 基于数组的实现
- (1) 普通数组队列
- (2) 循环队列(常基于数组实现,简单高效)
- 三、队列的基本操作
- 1. 初始化队列
- 2. 销毁队列
- 3. 入队操作
- 4. 出队操作
- 5. 获取队列元素数量
- 6. 检查队列是否为空
- 7. 获取队头元素
- 完整代码实现
- 四、数组队列与链式队列的比较
- 五、实际应用
- 六、注意事项
- 七、总结
前言
本文介绍c语言实现队列的相关内容。
(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手——通义,DeepSeek等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)
C语言实现队列的详细知识
一、队列的基本概念
队列(Queue)是一种特殊的线性数据结构(特殊线性表),遵循先进先出(FIFO,First In First Out)的原则。这意味着最早被添加到队列中的元素将是最先被移除的元素。(可以类比景区排队等候时的队列)
队列的特性
- 队头(Front):进行删除操作的一端
- 队尾(Rear):进行插入操作的一端
- 基本操作:
- 入队(Enqueue):在队尾插入元素
- 出队(Dequeue):从队头删除元素
队列在任务调度、消息队列、缓冲区处理等场景中广泛应用。
二、队列的实现方式
1. 基于链表的实现(常用)
链式队列使用链表数据结构来存储队列元素,避免了数组实现的容量限制和"假溢出"问题。
链式队列结构体:
typedef int QDataType; // 队列存储数据类型typedef struct QueueNode {QDataType val; // 存储的数据struct QueueNode *next; // 指向下一个节点的指针
} QueueNode;typedef struct Queue {QueueNode *head; // 队头指针QueueNode *tail; // 队尾指针
} Queue;
初始化:
void QueueInit(Queue *pq) {pq->head = pq->tail = NULL;
}
入队操作:
void QueuePush(Queue *pq, QDataType x) {QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));if (newNode == NULL) {perror("malloc failed");exit(1);}newNode->val = x;newNode->next = NULL;// 如果是空队列,更新head和tailif (pq->tail == NULL) {pq->head = pq->tail = newNode;} else {pq->tail->next = newNode;pq->tail = newNode;}
}
出队操作:
void QueuePop(Queue *pq) {if (pq->head == NULL) {printf("Queue is empty!\n");return;}QueueNode *toDelete = pq->head;pq->head = pq->head->next;// 如果队列中只剩一个元素,更新tailif (pq->head == NULL) {pq->tail = NULL;}free(toDelete);
}
销毁队列:
void QueueDestroy(Queue *pq) {QueueNode *cur = pq->head;while (cur) {QueueNode *next = cur->next;free(cur);cur = next;}pq->head = pq->tail = NULL;
}
2. 基于数组的实现
(1) 普通数组队列
普通数组队列在实现时,队头和队尾会不断后移,当队尾到达数组末尾时,无法继续在队尾插入元素,导致"假溢出"问题。
// 普通数组队列示例
#define MAXSIZE 20
typedef struct {int data[MAXSIZE];int front; // 队头指针int rear; // 队尾指针
} SqQueue;// 初始化
void InitQueue(SqQueue *Q) {Q->front = Q->rear = 0;
}
(2) 循环队列(常基于数组实现,简单高效)
循环队列(Circular Queue)是一种基于数组实现的队列数据结构,它通过将数组的首尾相连形成一个环形结构,使得队头和队尾指针在数组空间内循环移动。这种设计解决了普通队列的"假溢出"问题,实现了对存储空间的高效利用。
补充:普通队列的假溢出问题示例
假设有一个大小为5的数组队列,初始状态:
索引: 0 1 2 3 4
数据: - - - - -
front=0, rear=0
入队3个元素后:
索引: 0 1 2 3 4
数据: 1 2 3 - -
front=0, rear=3
出队2个元素后:
索引: 0 1 2 3 4
数据: - - 3 - -
front=2, rear=3
此时队列还有2个空位(索引0和1),但队尾指针rear=3已接近数组末尾,无法再入队,这就是"假溢出"。
关键特点:
- 队空条件:
front == rear - 队满条件:
(rear + 1) % MAXSIZE == front - 为区分队空和队满,通常少用一个存储空间(实际能存储MAXSIZE-1个元素)
为什么需要多开一个空间?
这是循环队列的关键设计点。为了区分队空和队满,我们特意多开一个空间(即数组大小 = k + 1,其中k是队列最多能存储的元素数量)。如果没有多开一个空间,当队列满时,front == rear,与队空条件冲突,无法区分。
循环队列的核心操作
- 初始化
typedef struct {int *arr; // 存储队列元素的数组int front; // 队头指针int rear; // 队尾指针int k; // 队列容量(最多存储k个元素)
} MyCircularQueue;MyCircularQueue* myCircularQueueCreate(int k) {MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));obj->arr = (int*)malloc(sizeof(int) * (k + 1)); // 多开一个空间obj->front = 0;obj->rear = 0;obj->k = k;return obj;
}
- 判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {return obj->front == obj->rear;
}
- 判满
bool myCircularQueueIsFull(MyCircularQueue* obj) {return (obj->rear + 1) % (obj->k + 1) == obj->front;
}
- 入队操作
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {if (myCircularQueueIsFull(obj)) {return false; // 队列已满}obj->arr[obj->rear] = value; // 在rear位置插入元素obj->rear = (obj->rear + 1) % (obj->k + 1); // rear指针循环移动return true;
}
- 出队操作
bool myCircularQueueDeQueue(MyCircularQueue* obj) {if (myCircularQueueIsEmpty(obj)) {return false; // 队列为空}obj->front = (obj->front + 1) % (obj->k + 1); // front指针循环移动return true;
}
- 获取队头元素
int myCircularQueueFront(MyCircularQueue* obj) {if (myCircularQueueIsEmpty(obj)) {// 通常返回一个特殊值或抛出异常return -1;}return obj->arr[obj->front];
}
- 获取队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {if (myCircularQueueIsEmpty(obj)) {return -1;}// rear指向的是最后一个元素的下一个位置,所以队尾元素在rear-1位置return obj->arr[(obj->rear - 1 + obj->k + 1) % (obj->k + 1)];
}
- 销毁队列
void myCircularQueueFree(MyCircularQueue* obj) {free(obj->arr);free(obj);
}
-
循环队列的优缺点
-
优点
- 空间利用率高:通过环形结构重复利用空位,避免"假溢出"。
- 操作效率高:入队和出队操作仅需指针移动,无元素搬移开销。
- 内存稳定:固定容量设计避免动态扩容带来的内存碎片。
- 实现简单:基于数组实现,指针操作清晰。
-
缺点
- 容量固定:无法动态扩展,需要预先确定队列大小。
- 少量空间浪费:为区分队空队满,需要多开一个空间。
- 内存碎片:在极端情况下,可能会有少量内存浪费。
-
三、队列的基本操作
1. 初始化队列
- 为队列分配内存空间
- 设置队头和队尾指针
- 对于循环队列,初始化front和rear为0
2. 销毁队列
- 释放队列中所有节点的内存
- 将队头和队尾指针置为NULL
3. 入队操作
- 在队尾插入新元素
- 对于循环队列,需要处理队尾指针的循环
4. 出队操作
- 从队头删除元素
- 对于循环队列,需要处理队头指针的循环
5. 获取队列元素数量
- 对于数组队列:
(rear - front + MAXSIZE) % MAXSIZE - 对于链式队列:需要额外维护一个计数器
6. 检查队列是否为空
- 对于数组队列:
front == rear - 对于链式队列:
head == NULL
7. 获取队头元素
- 返回队头元素的值,不修改队头指针
- 需要先检查队列是否为空
完整代码实现
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>// 定义队列中存储的数据类型为整数
typedef int QDataType;// 队列节点结构体定义
typedef struct QueueNode
{int val; // 存储的元素值struct QueueNode* next; // 指向下一个节点的指针
}QNode;// 队列结构体定义(包含头指针、尾指针和元素数量)
typedef struct Queue
{QNode* phead; // 队头指针(指向第一个元素)QNode* ptail; // 队尾指针(指向最后一个元素)int size; // 队列中元素的数量
}Queue;// 函数声明(队列操作接口)
void QueueInit(Queue* pq); // 初始化队列
void QueueDestroy(Queue* pq); // 销毁队列(释放内存)
void QueuePush(Queue* pq, QDataType x); // 入队操作
void QueuePop(Queue* pq); // 出队操作
QDataType QueueFront(Queue* pq); // 获取队头元素
QDataType QueueBack(Queue* pq); // 获取队尾元素
bool QueueEmpty(Queue* pq); // 检查队列是否为空
int QueueSize(Queue* pq); // 获取队列元素数量/* * 函数:QueueInit* 功能:初始化队列* 参数:pq - 指向队列结构体的指针* 返回值:无* 说明:将队头、队尾指针置为NULL,元素数量置为0*/
void QueueInit(Queue* pq)
{assert(pq); // 确保指针不为空pq->phead = NULL;pq->ptail = NULL;pq->size = 0;
}/* * 函数:QueueDestroy* 功能:销毁队列(释放所有节点内存)* 参数:pq - 指向队列结构体的指针* 返回值:无* 说明:遍历链表释放所有节点内存,然后重置队列状态*/
void QueueDestroy(Queue* pq)
{assert(pq); // 确保指针不为空QNode* cur = pq->phead; // 从队头开始遍历while (cur){QNode* next = cur->next; // 保存下一个节点指针free(cur); // 释放当前节点cur = next; // 移动到下一个节点}// 重置队列状态pq->phead = pq->ptail = NULL;pq->size = 0;
}/* * 函数:QueuePush* 功能:将元素插入队尾(入队操作)* 参数:pq - 指向队列结构体的指针* x - 要插入的元素值* 返回值:无* 说明:1. 分配新节点内存* 2. 处理空队列和非空队列两种情况* 3. 更新队尾指针和元素数量*/
void QueuePush(Queue* pq, QDataType x)
{assert(pq); // 确保指针不为空// 分配新节点内存QNode* newnode = (QNode*)malloc(sizeof(QNode));if (newnode == NULL){perror("malloc fail"); // 打印错误信息return;}newnode->val = x; // 设置节点值newnode->next = NULL; // 新节点是队尾,next指向NULL// 情况1:队列为空(无元素)if (pq->ptail == NULL){pq->phead = pq->ptail = newnode; // 队头和队尾都指向新节点}// 情况2:队列非空(已有元素)else{pq->ptail->next = newnode; // 将当前队尾的next指向新节点pq->ptail = newnode; // 更新队尾指针}pq->size++; // 元素数量加1
}/* * 函数:QueuePop* 功能:移除队头元素(出队操作)* 参数:pq - 指向队列结构体的指针* 返回值:无* 说明:1. 检查队列是否为空(使用assert暴力检查)* 2. 处理单节点队列和多节点队列两种情况* 3. 更新队头指针和元素数量*/
void QueuePop(Queue* pq)
{assert(pq); // 确保指针不为空assert(pq->phead != NULL); // 确保队列非空(暴力检查)// 情况1:队列只有一个元素if (pq->phead->next == NULL){free(pq->phead); // 释放队头节点pq->phead = pq->ptail = NULL; // 重置队头和队尾}// 情况2:队列有多个元素else{QNode* next = pq->phead->next; // 保存原队头的下一个节点free(pq->phead); // 释放原队头节点pq->phead = next; // 更新队头指针}pq->size--; // 元素数量减1
}/* * 函数:QueueFront* 功能:获取队头元素的值(不移除元素)* 参数:pq - 指向队列结构体的指针* 返回值:队头元素的值* 说明:1. 检查队列是否为空* 2. 返回队头节点的值*/
QDataType QueueFront(Queue* pq)
{assert(pq); // 确保指针不为空assert(pq->phead != NULL); // 确保队列非空return pq->phead->val; // 返回队头元素的值
}/* * 函数:QueueBack* 功能:获取队尾元素的值(不移除元素)* 参数:pq - 指向队列结构体的指针* 返回值:队尾元素的值* 说明:1. 检查队列是否为空* 2. 返回队尾节点的值*/
QDataType QueueBack(Queue* pq)
{assert(pq); // 确保指针不为空assert(pq->ptail != NULL); // 确保队列非空return pq->ptail->val; // 返回队尾元素的值
}/* * 函数:QueueEmpty* 功能:检查队列是否为空* 参数:pq - 指向队列结构体的指针* 返回值:true(队列为空)或false(队列非空)* 说明:通过比较元素数量判断队列是否为空*/
bool QueueEmpty(Queue* pq)
{assert(pq); // 确保指针不为空return pq->size == 0; // 如果size为0则队列为空
}/* * 函数:QueueSize* 功能:获取队列中元素的数量* 参数:pq - 指向队列结构体的指针* 返回值:队列中元素的数量* 说明:直接返回队列结构体中的size字段*/
int QueueSize(Queue* pq)
{assert(pq); // 确保指针不为空return pq->size; // 返回元素数量
}/* * 主函数:测试队列实现* 功能:演示队列的基本操作* 说明:1. 初始化队列* 2. 入队1、2* 3. 打印队头(1)* 4. 出队(移除1)* 5. 入队3、4* 6. 依次出队并打印(2,3,4)* 7. 销毁队列*/
int main()
{Queue q;QueueInit(&q);QueuePush(&q, 1);QueuePush(&q, 2);printf("%d ", QueueFront(&q)); // 输出:1QueuePop(&q); // 移除队头元素1QueuePush(&q, 3);QueuePush(&q, 4);// 依次出队并打印所有元素while (!QueueEmpty(&q)){printf("%d ", QueueFront(&q)); // 打印队头元素QueuePop(&q); // 移除队头元素}// 输出:2 3 4QueueDestroy(&q);return 0;
}
四、数组队列与链式队列的比较
| 特性 | 数组队列(循环队列) | 链式队列 |
|---|---|---|
| 存储空间 | 预先分配固定大小 | 动态分配,无需预先指定容量 |
| 空间利用率 | 可能有空间浪费(少用一个位置区分队空队满) | 高效利用空间 |
| 实现复杂度 | 简单,但需处理循环逻辑 | 相对简单 |
| 操作时间复杂度 | O(1) | O(1) |
| 队列大小限制 | 有最大容量 | 仅受内存限制 |
| 内存效率 | 无额外指针开销 | 每个节点有指针开销 |
| 适用场景 | 预知队列大小,需要高效访问 | 队列大小不确定,需要动态扩展 |
五、实际应用
队列在计算机科学中有广泛的应用:
- 任务调度:操作系统中的进程调度
- 消息队列:用于解耦系统组件,如RabbitMQ、Kafka
- 缓冲区:如打印机队列、网络数据包处理
- 广度优先搜索:图的遍历算法
- 打印任务管理:多个用户提交的打印任务排队处理
六、注意事项
-
循环队列的队空与队满区分:
- 通常采用"少用一个存储空间"的方法
- 也可以使用额外的计数器来记录队列元素数量
-
链式队列的内存管理:
- 必须在队列销毁时释放所有节点内存
- 防止内存泄漏
-
线程安全:
- 在多线程环境下,需要考虑队列操作的同步问题
- 可以使用互斥锁等机制保证线程安全
七、总结
C语言实现队列主要有两种方式:基于数组的循环队列和基于链表的链式队列。循环队列适合队列大小已知的场景,而链式队列则更适合队列大小不确定的场景。
无论哪种实现方式,队列都保持了其先进先出的特性,使得它在处理需要按顺序处理的元素序列时非常有用。在实际应用中,需要根据具体需求选择合适的实现方式,并注意处理队空、队满等边界条件。
