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