C语言实现队列数据结构:从顺序队列到循环队列的完整代码指南

📅 2026/8/23 13:15:43
C语言实现队列数据结构:从顺序队列到循环队列的完整代码指南
这次我们来看一个数据结构中的基础但极其重要的概念队列。对于初学者来说队列的“先进先出”原理听起来简单但真正动手实现时结构体怎么设计初始化要注意什么如何判断空和满循环队列又是怎么回事它的判空判满条件为什么容易混淆这些问题往往是学习路上的第一个小坎。这篇文章不讲复杂的理论推导直接聚焦于“能不能用代码跑起来”和“怎么用代码实现”。我们会从零开始用C语言一步步构建一个完整的队列涵盖顺序队列和循环队列两种实现重点拆解结构体设计、初始化、判空、判满、入队、出队等核心操作。你将看到清晰的代码示例、每一步的逻辑解释以及最关键的那些“坑点”和调试方法。无论你是正在准备数据结构考试还是希望在项目中应用队列这篇内容都能帮你快速建立可运行的代码模板和清晰的排查思路。1. 核心能力速览在深入代码之前我们先快速了解本文将实现的两个队列版本的核心特性和区别。能力项顺序队列 (非循环)循环队列数据结构数组 头尾指针 (front,rear)数组 头尾指针 (front,rear)存储方式线性存储尾指针指向最后一个元素的下一个位置环形存储利用取模运算实现空间复用空间利用率低存在“假溢出”现象高有效利用数组空间判空条件front rearfront rear判满条件rear MAX_SIZE(无法区分空和真满)(rear 1) % MAX_SIZE front(牺牲一个单元)入队操作data[rear] valuedata[rear] value; rear (rear 1) % MAX_SIZE出队操作value data[front]value data[front]; front (front 1) % MAX_SIZE适合场景理解队列基本原理元素总量固定且已知实际应用需要高效利用内存的缓冲区、任务队列等本文重点我们将先实现一个基础的顺序队列来理解流程然后重点攻克循环队列的设计特别是其独特的判空判满逻辑这是面试和笔试中的高频考点。2. 适用场景与使用边界队列Queue是一种操作受限的线性表其“先进先出”FIFO的特性使其在众多场景中不可或缺。适合谁计算机专业学生应对数据结构课程、期末考试、考研复试。初级开发者理解消息队列、任务调度等系统设计的基础。算法爱好者在广度优先搜索BFS等算法中队列是核心数据结构。能解决什么问题任务调度操作系统中的进程就绪队列、打印任务队列。消息缓冲生产者和消费者模式下的消息队列如Kafka、RabbitMQ的底层思想。数据流处理网络数据包接收缓冲区、音视频播放缓冲区。广度优先搜索BFS遍历树或图时用于存储待访问的节点。不适合什么场景需要随机访问元素队列只允许在两端操作不支持通过索引直接访问中间元素。需要后进先出LIFO逻辑这应该使用栈Stack。元素优先级不同需要优先队列Priority Queue或堆Heap。使用边界与注意事项内存管理本文示例使用静态数组大小固定。在实际应用中可能需要动态扩容如使用链表实现或动态数组。线程安全本文示例代码未考虑多线程并发访问。在并发环境下入队和出队操作需要加锁或使用线程安全队列。数据持久化内存中的队列数据在程序退出后会丢失。需要持久化的队列应基于数据库或文件系统实现。3. 环境准备与前置条件本文将使用最经典的C语言进行实现确保代码的通用性和可移植性。你只需要一个能编译运行C代码的环境即可。通用环境检查清单操作系统Windows, Linux, macOS 均可。编译器GCC (Linux/macOS) 或 MinGW (Windows) 是推荐选择。确保gcc --version命令可以执行。开发工具任何文本编辑器如VS Code, Sublime Text, Vim或集成开发环境如Code::Blocks, Dev-C, CLion。基础知识了解C语言基础语法特别是数组、结构体、指针和函数。项目结构预览 我们将创建两个主要的C文件来分别演示顺序队列和循环队列。queue_demo/ ├── sequential_queue.c # 顺序队列实现 └── circular_queue.c # 循环队列实现 (重点)4. 结构体设计与初始化队列的核心是数据存储和状态标识。我们使用结构体将它们封装在一起。4.1 顺序队列的结构体与初始化顺序队列使用一个数组和两个整型指针或下标来管理。// sequential_queue.c #include stdio.h #include stdlib.h #include stdbool.h // 使用bool类型 #define MAX_SIZE 5 // 队列最大容量 // 定义顺序队列结构体 typedef struct { int data[MAX_SIZE]; // 静态数组存储元素 int front; // 队头指针下标 int rear; // 队尾指针下标指向下一个待插入位置 } SequentialQueue; // 初始化队列 void initSequentialQueue(SequentialQueue *q) { if (q NULL) { printf(队列指针为空\n); return; } q-front 0; q-rear 0; printf(顺序队列初始化成功。front%d, rear%d\n, q-front, q-rear); } // 判断队列是否为空 bool isSequentialQueueEmpty(SequentialQueue *q) { return q-front q-rear; } // 判断队列是否已满对于非循环队列这个判断有问题 bool isSequentialQueueFull(SequentialQueue *q) { return q-rear MAX_SIZE; // 注意这个判满条件无法处理“假溢出” }关键点解析front和rear初始都指向0 (data[0]之前的位置)。rear始终指向下一个可以插入元素的位置。初始状态front rear队列为空。非循环队列的致命问题当rear MAX_SIZE时即使数组前面有空位front 0也无法再插入新元素这种现象称为“假溢出”。这正是我们需要循环队列的原因。4.2 循环队列的结构体与初始化循环队列通过取模运算让数组在逻辑上首尾相连。// circular_queue.c #include stdio.h #include stdlib.h #include stdbool.h #define MAX_SIZE 5 // 队列最大容量 // 定义循环队列结构体 typedef struct { int data[MAX_SIZE]; // 静态数组存储元素 int front; // 队头指针 int rear; // 队尾指针指向下一个待插入位置 } CircularQueue; // 初始化循环队列 void initCircularQueue(CircularQueue *q) { if (q NULL) { printf(队列指针为空\n); return; } q-front 0; q-rear 0; printf(循环队列初始化成功。front%d, rear%d\n, q-front, q-rear); }结构体设计看起来一样是的结构体成员一模一样。核心区别在于后续操作中对front和rear指针的移动逻辑。在循环队列中指针前进需要做取模运算pointer (pointer 1) % MAX_SIZE。5. 核心操作实现入队与出队这是队列的灵魂所在。我们分别实现顺序队列和循环队列的入队Enqueue和出队Dequeue操作。5.1 顺序队列的入队与出队// sequential_queue.c (续) // 入队操作 bool enqueueSequential(SequentialQueue *q, int value) { if (isSequentialQueueFull(q)) { printf(队列已满无法入队元素 %d。\n, value); return false; // 入队失败 } q-data[q-rear] value; // 在rear位置放入元素 q-rear; // rear指针后移 printf(元素 %d 入队成功。当前rear%d\n, value, q-rear); return true; } // 出队操作 bool dequeueSequential(SequentialQueue *q, int *value) { if (isSequentialQueueEmpty(q)) { printf(队列为空无法出队。\n); return false; // 出队失败 } *value q-data[q-front]; // 取出front位置的元素 q-front; // front指针后移 printf(元素 %d 出队成功。当前front%d\n, *value, q-front); return true; } // 打印队列当前状态用于调试 void printSequentialQueue(SequentialQueue *q) { printf(队列状态[); for (int i q-front; i q-rear; i) { printf(%d, q-data[i]); if (i q-rear - 1) { printf(, ); } } printf(]\n); printf(指针位置front%d, rear%d\n, q-front, q-rear); }测试一下顺序队列// sequential_queue.c (主函数测试) int main() { SequentialQueue q; int value; initSequentialQueue(q); // 测试入队 enqueueSequential(q, 10); enqueueSequential(q, 20); enqueueSequential(q, 30); printSequentialQueue(q); // 测试出队 dequeueSequential(q, value); printSequentialQueue(q); // 继续入队直到触发“假溢出” enqueueSequential(q, 40); enqueueSequential(q, 50); // 此时 rear5, 等于MAX_SIZE enqueueSequential(q, 60); // 这里会失败即使前面有空间(因为front1) printSequentialQueue(q); return 0; }运行结果分析 你会看到在入队元素60时程序报告“队列已满”但打印出的队列状态显示front1, rear5数组data[0]的位置其实是空闲的。这就是假溢出。循环队列就是为了解决这个问题。5.2 循环队列的入队、出队与判空判满循环队列的实现是本文的重中之重尤其是判满条件。// circular_queue.c (续) // 判断循环队列是否为空 bool isCircularQueueEmpty(CircularQueue *q) { // 空队列条件头尾指针相等 return q-front q-rear; } // 判断循环队列是否已满 bool isCircularQueueFull(CircularQueue *q) { // 满队列条件尾指针的下一个位置是头指针 // 牺牲一个存储单元来区分空和满的状态 return (q-rear 1) % MAX_SIZE q-front; } // 循环队列入队 bool enqueueCircular(CircularQueue *q, int value) { if (isCircularQueueFull(q)) { printf(循环队列已满无法入队元素 %d。\n, value); return false; } q-data[q-rear] value; // 放入元素 q-rear (q-rear 1) % MAX_SIZE; // rear循环后移 printf(元素 %d 入队成功。当前rear%d\n, value, q-rear); return true; } // 循环队列出队 bool dequeueCircular(CircularQueue *q, int *value) { if (isCircularQueueEmpty(q)) { printf(循环队列为空无法出队。\n); return false; } *value q-data[q-front]; // 取出元素 q-front (q-front 1) % MAX_SIZE; // front循环后移 printf(元素 %d 出队成功。当前front%d\n, *value, q-front); return true; } // 获取循环队列中的元素个数 int getCircularQueueSize(CircularQueue *q) { return (q-rear - q-front MAX_SIZE) % MAX_SIZE; } // 打印循环队列状态逻辑视图 void printCircularQueue(CircularQueue *q) { if (isCircularQueueEmpty(q)) { printf(队列状态[] (空)\n); } else { printf(队列状态[); int i q-front; while (i ! q-rear) { printf(%d, q-data[i]); i (i 1) % MAX_SIZE; if (i ! q-rear) { printf(, ); } } printf(]\n); } printf(指针位置front%d, rear%d, 元素个数%d\n, q-front, q-rear, getCircularQueueSize(q)); }核心逻辑拆解判空front rear。和顺序队列一样。判满(rear 1) % MAX_SIZE front。这是最关键也是最容易出错的地方。我们故意牺牲了一个存储单元来区分队列“空”和“满”的状态。这意味着一个大小为MAX_SIZE的数组最多只能存放MAX_SIZE - 1个有效元素。指针移动所有对front和rear的1操作都必须伴随取模运算% MAX_SIZE以实现“循环”。计算元素个数由于是循环的不能简单用rear - front。公式(rear - front MAX_SIZE) % MAX_SIZE可以正确处理所有情况。6. 功能测试与效果验证让我们编写一个主函数来全面测试循环队列的所有边界情况。// circular_queue.c (主函数测试) int main() { CircularQueue q; int value; printf( 循环队列功能测试 \n); initCircularQueue(q); printCircularQueue(q); // 预期[] printf(\n1. 测试入队直到满...\n); for (int i 1; i 4; i) { // MAX_SIZE5, 最多存4个 enqueueCircular(q, i * 10); printCircularQueue(q); } // 尝试插入第5个元素应该失败 enqueueCircular(q, 50); printCircularQueue(q); printf(\n2. 测试出队两个元素...\n); dequeueCircular(q, value); printf(出队: %d\n, value); printCircularQueue(q); dequeueCircular(q, value); printf(出队: %d\n, value); printCircularQueue(q); printf(\n3. 继续入队测试循环特性...\n); enqueueCircular(q, 50); // 成功填充空位 printCircularQueue(q); enqueueCircular(q, 60); // 成功 printCircularQueue(q); enqueueCircular(q, 70); // 失败队列又满了 printCircularQueue(q); printf(\n4. 测试清空队列...\n); while (!isCircularQueueEmpty(q)) { dequeueCircular(q, value); printf(出队: %d\n, value); } printCircularQueue(q); // 预期[] printf(\n5. 测试从空队列出队...\n); dequeueCircular(q, value); // 应该失败 return 0; }编译与运行 在终端中使用gcc编译并运行gcc -o circular_queue circular_queue.c ./circular_queue预期输出分析 通过观察输出你可以清晰地看到初始队列为空。入队4个元素后队列满rear的下一个位置是front第5次入队失败。出队两个元素后front移动。再次入队时新元素50和60被填入数组开头的空位data[0]和data[1]rear指针从数组末尾“循环”到了开头。这解决了假溢出问题。最终队列被清空从空队列出队操作失败。这个测试完整覆盖了队列的初始化、判空、判满、入队、出队、循环特性以及错误处理。7. 资源占用与性能观察对于这种基础数据结构我们关注的“性能”主要是时间复杂度和空间复杂度以及代码的正确性与健壮性。时间复杂度分析操作顺序队列循环队列初始化O(1)O(1)判空/判满O(1)O(1)入队 (Enqueue)O(1)O(1)出队 (Dequeue)O(1)O(1)遍历/打印O(n)O(n)所有核心操作都是常数时间复杂度效率非常高。空间复杂度分析本文实现使用了静态数组空间复杂度为 O(n)其中 n 是MAX_SIZE。如果使用动态数组malloc可以在运行时决定队列大小。如果使用链表实现则可以动态增长但每个节点需要额外的指针空间。内存布局观察 对于静态数组实现的队列其内存是连续的访问速度快。front和rear是两个整型变量存储的是数组下标。在循环队列中指针的移动通过取模运算实现这是一个非常轻量的计算。如何验证你的实现是正确的单元测试像上面的主函数一样设计测试用例覆盖空队、满队、连续入队出队、循环边界等情况。调试打印在关键操作入队、出队前后打印front、rear和队列内容直观跟踪指针变化。压力测试可以写一个循环进行数万次的随机入队和出队操作检查队列状态是否始终一致例如入队总数 - 出队总数 当前队列大小。8. 常见问题与排查方法在实现和使用队列时你可能会遇到以下典型问题问题现象可能原因排查方式解决方案编译错误未定义标识符bool没有包含stdbool.h头文件。检查代码开头#include部分。添加#include stdbool.h。运行时崩溃段错误向队列函数传递了空指针 (NULL)。在函数入口处检查指针是否为NULL。在init,enqueue,dequeue等函数开始处添加if (q NULL) return;。队列行为异常数据错乱1. 判满条件写错。2. 指针移动后未取模循环队列。3.front或rear越界。1. 复核isFull函数逻辑。2. 检查rear (rear 1) % size是否正确。3. 在每次操作后打印指针和队列内容。1. 牢记循环队列判满公式(rear1)%size front。2. 确保所有指针前进都进行取模运算。3. 使用调试器或打印语句逐步跟踪。“假溢出”问题使用了顺序队列当rear MAX_SIZE但front 0时无法再入队。观察front和rear的值。如果front不在0但rear已到最大就是此问题。改用循环队列实现。队列大小计算错误循环队列中使用了rear - front计算。当rear front时循环后rear - front为负数。使用正确公式(rear - front MAX_SIZE) % MAX_SIZE。无法区分队列空和满循环队列的判空和判满条件都是front rear。当队列满时rear会追上front导致状态混淆。牺牲一个存储单元使判满条件为(rear1)%size front。内存泄漏动态分配时链表实现的队列出队时只移动指针未释放节点内存。检查dequeue函数是否在移除节点后调用了free()。在链表出队操作中先保存要删除的节点移动front指针再释放该节点内存。最重要的调试技巧可视化你的队列。在每一步操作入队/出队后都调用一个打印函数输出front、rear的当前值以及数组中的所有元素可以标记出front和rear的位置。这对于理解循环队列的指针移动规律至关重要。9. 最佳实践与使用建议掌握了基础实现后以下建议能帮助你在项目和面试中更好地运用队列。封装与接口化将队列结构体和所有操作函数放在独立的头文件.h和源文件.c中。这提高了代码的模块化和可复用性。// queue.h #ifndef QUEUE_H #define QUEUE_H typedef struct { ... } Queue; void initQueue(Queue *q); bool enqueue(Queue *q, int value); // ... 其他函数声明 #endif考虑泛型本文队列存储的是int类型。在实际应用中你可能需要存储任意类型的数据。可以使用void*指针牺牲类型安全或C的模板。动态扩容静态数组大小固定。对于未知数据量的场景可以实现动态扩容的队列。当队列满时申请一个更大的数组将原有数据拷贝过去注意循环队列数据的拷贝需要特殊处理。线程安全版本如果在多线程环境下使用需要对入队和出队操作加锁如互斥锁pthread_mutex_t或者直接使用线程安全的数据结构库。选择正确的实现数组循环队列元素数量上限已知或可预估追求高性能和缓存友好性时使用。链表队列元素数量不可预知需要频繁动态增删时使用。它没有容量限制除了内存本身但每个节点有额外开销。理解牺牲单元判满法这是最常用的循环队列判满策略简单可靠。务必理解其原理并能手写推导。面试常考。用于BFS算法队列是BFS算法的标准组件。熟练实现队列能让你更专注于BFS算法逻辑本身。// BFS 伪代码示例 Queue q; initQueue(q); enqueue(q, start_node); while (!isQueueEmpty(q)) { Node current dequeue(q); // 处理当前节点... for (each neighbor of current) { if (neighbor not visited) { enqueue(q, neighbor); } } }10. 总结与下一步通过从顺序队列到循环队列的逐步实现我们彻底解决了“假溢出”问题并掌握了队列这一核心数据结构的完整操作方法。循环队列中牺牲一个存储单元来判满的设计以及取模运算实现指针循环的技巧是必须理解并能够手写的关键。最值得尝试的点将本文的int类型队列改为泛型队列支持存储结构体。用链表实现一个动态队列并比较其与数组队列的优缺点。尝试实现一个“计数法”判满的循环队列不牺牲存储单元但增加一个计数变量。最先应该验证的功能 一定要自己敲一遍循环队列的代码并用我们提供的测试用例跑通。重点观察front和rear指针在数组末尾是如何“绕回”开头的这是理解循环队列的钥匙。最容易踩的坑判满条件写错这是最高频错误务必记住(rear 1) % size front。指针移动忘记取模在循环队列中任何front或rear都必须替换为front (front 1) % size。计算元素个数公式错误使用(rear - front MAX_SIZE) % MAX_SIZE。队列是构建更复杂系统如消息队列、任务调度器的基石。理解其原理和实现细节不仅能帮助你通过考试更能为后续学习操作系统、网络、分布式系统打下坚实基础。建议将本文的代码作为模板收藏在需要时快速回顾和复用。