代码改变世界

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

2025-11-23 10:29  tlnshuju  阅读(0)  评论(0)    收藏  举报


前言

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

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


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

一、队列的基本概念

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

队列的特性

  1. 队头(Front):进行删除操作的一端
  2. 队尾(Rear):进行插入操作的一端
  3. 基本操作
    • 入队(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和tail
if (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;
// 如果队列中只剩一个元素,更新tail
if (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,与队空条件冲突,无法区分。

循环队列的核心操作

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

  • 返回队头元素的值,不修改队头指针
  • 需要先检查队列是否为空

完整代码实现

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

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