c语言实现队列【由浅入深-数据结构】

文章目录


前言

本文介绍c语言实现队列的相关内容。

(【由浅入深】是一个系列文章,它记录了我个人作为一个小白,在学习c++技术开发方向计相关知识过程中的笔记,欢迎各位彭于晏刘亦菲从中指出我的错误并且与我共同学习进步,作为该系列的第一部曲-c语言,大部分知识会根据本人所学和我的助手------通义,DeepSeek等以及合并网络上所找到的相关资料进行核实誊抄,每一篇文章都可能会因为一些错误在后续时间增删改查,因为该系列按照我的网络课程学习笔记形式编写,我会使用绝大多数人使用的讲解顺序编写,所以基础框架和大部分内容案例会与他人一样,基础知识不会过于详细讲述)


C语言实现队列的详细知识

一、队列的基本概念

队列(Queue)是一种特殊的线性数据结构(特殊线性表),遵循先进先出(FIFO,First In First Out)的原则。这意味着最早被添加到队列中的元素将是最先被移除的元素。(可以类比景区排队等候时的队列)

队列的特性

  1. 队头(Front):进行删除操作的一端
  2. 队尾(Rear):进行插入操作的一端
  3. 基本操作
    • 入队(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,与队空条件冲突,无法区分。

循环队列的核心操作

  1. 初始化
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;
}
  1. 判空
c 复制代码
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->front == obj->rear;
}
  1. 判满
c 复制代码
bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return (obj->rear + 1) % (obj->k + 1) == obj->front;
}
  1. 入队操作
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;
}
  1. 出队操作
c 复制代码
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj)) {
        return false; // 队列为空
    }
    obj->front = (obj->front + 1) % (obj->k + 1); // front指针循环移动
    return true;
}
  1. 获取队头元素
c 复制代码
int myCircularQueueFront(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj)) {
        // 通常返回一个特殊值或抛出异常
        return -1;
    }
    return obj->arr[obj->front];
}
  1. 获取队尾元素
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)];
}
  1. 销毁队列
c 复制代码
void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->arr);
    free(obj);
}
  • 循环队列的优缺点

    • 优点

      1. 空间利用率高:通过环形结构重复利用空位,避免"假溢出"。
      2. 操作效率高:入队和出队操作仅需指针移动,无元素搬移开销。
      3. 内存稳定:固定容量设计避免动态扩容带来的内存碎片。
      4. 实现简单:基于数组实现,指针操作清晰。
    • 缺点

      1. 容量固定:无法动态扩展,需要预先确定队列大小。
      2. 少量空间浪费:为区分队空队满,需要多开一个空间。
      3. 内存碎片:在极端情况下,可能会有少量内存浪费。

三、队列的基本操作

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)
队列大小限制 有最大容量 仅受内存限制
内存效率 无额外指针开销 每个节点有指针开销
适用场景 预知队列大小,需要高效访问 队列大小不确定,需要动态扩展

五、实际应用

队列在计算机科学中有广泛的应用:

  1. 任务调度:操作系统中的进程调度
  2. 消息队列:用于解耦系统组件,如RabbitMQ、Kafka
  3. 缓冲区:如打印机队列、网络数据包处理
  4. 广度优先搜索:图的遍历算法
  5. 打印任务管理:多个用户提交的打印任务排队处理

六、注意事项

  1. 循环队列的队空与队满区分

    • 通常采用"少用一个存储空间"的方法
    • 也可以使用额外的计数器来记录队列元素数量
  2. 链式队列的内存管理

    • 必须在队列销毁时释放所有节点内存
    • 防止内存泄漏
  3. 线程安全

    • 在多线程环境下,需要考虑队列操作的同步问题
    • 可以使用互斥锁等机制保证线程安全

七、总结

C语言实现队列主要有两种方式:基于数组的循环队列和基于链表的链式队列。循环队列适合队列大小已知的场景,而链式队列则更适合队列大小不确定的场景。

无论哪种实现方式,队列都保持了其先进先出的特性,使得它在处理需要按顺序处理的元素序列时非常有用。在实际应用中,需要根据具体需求选择合适的实现方式,并注意处理队空、队满等边界条件。

相关推荐
要一起看日出3 小时前
数据结构---------红黑树
java·数据结构·红黑树
大数据张老师4 小时前
数据结构——折半查找
数据结构·算法·查找·折半查找
m0_626535204 小时前
数据结构学习,一些知识点
数据结构·学习
敲代码的瓦龙4 小时前
C语言?大小端!!!
c语言·开发语言·c++·1024程序员节
想唱rap5 小时前
C++list类的模拟实现
linux·运维·服务器·数据结构·c++·windows·list
Yurko135 小时前
【C语言】程序控制结构
c语言·开发语言·学习
say_fall5 小时前
数据结构之顺序表:一款优秀的顺序存储结构
c语言·数据结构
yuuki2332337 小时前
【数据结构】顺序表的实现
c语言·数据结构·后端
GilgameshJSS7 小时前
STM32H743-ARM例程31-CAN
c语言·arm开发·stm32·单片机·嵌入式硬件