
数据结构这门课很多人卡住的地方不在概念本身而是怎么把概念落到卷面上、落到代码里。尤其是循环队列、指针、栈这三个点几乎每个学校期末、考研题、面试笔试里都会出现但每次遇到题目还是会有人分不清front到底指向队头元素还是队头前一个位置rear到底存的是什么栈空和队满的条件为什么长得这么像。这次我们就围绕“数据结构题目解析循环队列、指针、栈”把这一整块高频考点拆开讲。不只是背公式而是把它拆成“题型长什么样 - 为什么这么设计 - 代码怎么实现 - 考试怎么考 - 调试怎么查”这条完整链路。过程中我会给出一套可以自建的“解题栈 Hub”也就是一个用于沉淀题目、题解、易错点和代码模板的仓库结构让刷过的题不白刷。文章内容会覆盖三部分循环队列的数组实现与判空判满、栈的顺序存储和链式存储、指针在链表和动态内存管理中的典型用法。中间会穿插可复制的 C 语言代码、一道题目从读题到 AC 的完整分析思路、以及一套通用调试方法。如果你是期末复习、考研冲刺或者准备面试手撕题这篇可以直接拿去对照复习。1. 核心能力速览能力项说明内容主题数据结构高频考点循环队列、栈、指针代码语言C 语言为主部分思路可迁移到 C/Java适用人群本科生期末复习、考研数据结构、面试手撕算法核心能力提升循环队列判空判满、栈的多种实现、指针与链表代码运行环境GCC/Clang 任意文本编辑器或 IDE内存检查工具ValgrindLinux、VS 调试器Windows配套扩展自建“解题栈 Hub”题目管理目录是否需要 GPU不需要是否支持窗口界面不需要命令行即可批量处理能力对应“批量刷题 批量自测”通过脚本统一编译运行2. 适用场景与使用边界2.1 适合谁这门课的典型读者有三类正在准备数据结构期末考试的本科生。期末考试的重点通常就落在“线性表、栈、队列、树、图、查找、排序”上而循环队列、栈和指针又是线性结构里最容易出大题的模块。准备 408 考研或者自主命题考研的考生。历年后台题很喜欢考“循环队列的元素个数计算”“栈在表达式求值中的应用”“链表的指针修改顺序”。这些内容不是背两遍就能拿分的必须手写代码。准备校招面试手撕算法的人。虽然面试主流是 LeetCode但很多公司会考察基础结构实现比如“用数组实现一个支持动态扩容的栈”“用两个栈模拟队列”“写出循环队列的判满条件”。2.2 能解决什么问题搞清循环队列的判空判满为什么还要单独讨论而不是直接用front rear判断。搞清顺序栈和链栈的选择依据。搞清指针在动态链表里为什么需要二级指针或者头节点。建立一套可复用的“题目 题解 代码 易错点”管理方式也就是解题栈 Hub 的雏形。2.3 不适合什么场景如果你想要的是“快速背完拿高分”这篇文章对你会有点绕。这里强调为什么和怎么调试无法替代刷题量。如果你完全不想写代码只想看文字结论代码段可以跳过但核心公式和结论还是要看。如果你在找某种“自动生成题解”的工具这里没有也不建议用那样的方式来复习数据结构。2.4 学习与使用边界文中的代码是教学示例可以直接用于本地练习但不要在在线评测系统上直接提交复制的结果而不做题目分析。涉及到指针操作时运行前必须确认内存分配成功、释放时机正确避免野指针和重复释放的问题。如果参考了严蔚敏《数据结构》教材、王道考研系列或其他公开题解转载、贴代码时注意版权说明。个人学习可以自由练习商用或大段复制需要遵守对应资源的使用规则。3. 环境准备与前置条件数据结构题目解析并不需要大型软件所有例子都可以在命令行下完成。这里给出一套通用环境准备清单。3.1 操作系统Linux、macOS、Windows 都可以。如果你使用 Windows推荐两种方式之一安装 WSL2 并配置 Ubuntu在 Linux 环境里使用 GCC。直接使用 Windows 自带的 MinGW-w64或者用 Visual Studio 创建一个 C 语言控制台工程。3.2 编译器C 语言建议用 GCC标准建议-stdc11。如果你在终端里测试可以先确认版本gcc --version如果没有安装 GCCUbuntu/Debian 上执行sudo apt update sudo apt install build-essentialmacOS 上安装 Xcode Command Line Toolsxcode-select --installWindows 下安装 MinGW-w64 后需要把bin目录加入 PATH或者直接在终端里调用完整路径。3.3 调试工具Linuxgdb用于单步调试。Linuxvalgrind用于检查内存泄漏和非法读写。WindowsVisual Studio 调试器或者 CodeRunner 也可以。安装示例sudo apt install gdb valgrind3.4 前置知识数组与结构体的基础知识。函数传参的基本方式值传递和地址传递。动态内存管理malloc/free理解“申请”和“释放”是成对操作。4. 循环队列核心考点与代码实现循环队列是数据结构里最容易被绕晕的知识点。它本身是一个先进先出的线性结构但因为底层用数组模拟所以必须处理“数组下标越界”和“队列空/满无法区分”这两个问题。4.1 为什么需要循环队列普通队列用数组实现时入队操作rear出队操作front问题在于数组前面被出队的位置不会再被利用最后会变成“数组后端满前端却空着”的假溢出。循环队列的核心思想是让数组下标在到达最大容量时回到 0变成一个环。队头指针和队尾指针都在环上移动。结构体定义通常长这样#define MAX_SIZE 100 typedef struct { int data[MAX_SIZE]; int front; int rear; } CircularQueue;这里的front和rear在写法上有两种习惯标准教材写法严蔚敏front指向队头元素rear指向队尾元素的下一个位置。另一种写法front指向队头元素的前一个位置rear指向队尾元素。大多数考研题和期末题默认使用第一种写法也就是“队尾指针指向入队时的插入位置”。下面的代码都基于这个约定。4.2 判空与判满在数组实现里当front rear时可能存在两种情况队列为空或者队列已满。如果不做额外处理无法区分。常见解决方案有三种牺牲一个存储单元。当(rear 1) % MAX_SIZE front时认为队满允许最多存储MAX_SIZE - 1个元素。设置size字段记录元素个数。入队时size出队时size--用size是否为 0 或MAX_SIZE判断空和满。设置tag标志位。入队时设置tag 1出队时设置tag 0如果front rear且tag 1则队满。本文以第 1 种方案为例这是考研题和主流教材使用最广泛的方式。判空条件int isEmpty(CircularQueue *queue) { return queue-front queue-rear; }判满条件int isFull(CircularQueue *queue) { return (queue-rear 1) % MAX_SIZE queue-front; }注意牺牲一个存储单元后队满时实际上还有最后一个空位但因为要区分空和满所以这个位置不能用。这个细节在期末试卷里经常作为辨析题出现。队列中元素个数公式count (queue-rear - queue-front MAX_SIZE) % MAX_SIZE;这个公式必须背到条件反射的程度。它同时适用于rear front和rear front两种情况。记住一个原则无论队尾指针是否已经绕回到数组前面rear - front后加一个MAX_SIZE再取模就能得到真正的元素数量。4.3 初始化、入队、出队初始化void initQueue(CircularQueue *queue) { queue-front 0; queue-rear 0; }入队int enqueue(CircularQueue *queue, int value) { if (isFull(queue)) { return 0; // 队满入队失败 } queue-data[queue-rear] value; queue-rear (queue-rear 1) % MAX_SIZE; return 1; }出队int dequeue(CircularQueue *queue, int *value) { if (isEmpty(queue)) { return 0; // 队空出队失败 } *value queue-data[queue-front]; queue-front (queue-front 1) % MAX_SIZE; return 1; }重点理解rear和front每次移动都是取模移动不是简单的自增。入队时元素写入data[rear]然后把rear向后移动一位出队时先把当前队头元素读出再把front向后移动一位。4.4 循环队列典型题目分析以一道常见的期末题为例设计一个循环队列容量为 6。初始front 2, rear 4。连续入队 3 个元素后再出队 2 个元素问此时队头指针和队尾指针分别指向哪里分析过程初始状态front 2, rear 4元素个数为(4 - 2 6) % 6 2也就是说数组下标 2 和 3 上有元素。连续入队 3 个元素每次入队后rear (rear 1) % 6第一次入队后rear 5第二次入队后rear 0第三次入队后rear 1此时front 2元素个数为(1 - 2 6) % 6 5。连续出队 2 个元素每次出队后front (front 1) % 6第一次出队后front 3第二次出队后front 4最终结果为front 4, rear 1元素个数为(1 - 4 6) % 6 3。这种题的技巧是把数组下标画成环形图每次移动都从当前点沿环向后走填到哪一格就标哪一格。只要不丢取模操作基本不会错。4.5 循环队列调试与验证建议用一个临时测试函数把每一步的执行结果打印出来#include stdio.h void printQueueStatus(CircularQueue *queue) { printf(front %d, rear %d, count %d\n, queue-front, queue-rear, (queue-rear - queue-front MAX_SIZE) % MAX_SIZE); } int main() { CircularQueue queue; initQueue(queue); enqueue(queue, 10); enqueue(queue, 20); enqueue(queue, 30); printQueueStatus(queue); int value; dequeue(queue, value); printf(dequeue: %d\n, value); printQueueStatus(queue); return 0; }运行结果应该能看出来入队后rear后移出队后front后移。用这种“每步打印指针状态”的方式调试循环队列比凭眼睛猜快得多。5. 栈核心考点与代码实现栈是“后进先出”的线性结构只能在一端进行插入和删除操作。栈的题目变化很多但底层实现只有两种顺序栈和链栈。5.1 顺序栈顺序栈使用数组存放数据用一个整数top记录栈顶位置。不同教材的top指向约定不完全一样这里采用最常用的约定top指向栈顶元素所在位置栈空时top -1。#define MAX_STACK_SIZE 100 typedef struct { int data[MAX_STACK_SIZE]; int top; } SeqStack;初始化void initStack(SeqStack *stack) { stack-top -1; }入栈int push(SeqStack *stack, int value) { if (stack-top MAX_STACK_SIZE - 1) { return 0; // 栈满 } stack-data[stack-top] value; return 1; }出栈int pop(SeqStack *stack, int *value) { if (stack-top -1) { return 0; // 栈空 } *value stack-data[stack-top--]; return 1; }取栈顶元素int peek(SeqStack *stack, int *value) { if (stack-top -1) { return 0; } *value stack-data[stack-top]; return 1; }5.2 链栈链栈是使用单链表实现的栈入栈相当于头插法插入节点出栈相当于删除头节点。链栈不需要判断栈满只要内存申请成功就可以继续入栈。typedef struct StackNode { int data; struct StackNode *next; } StackNode; typedef struct { StackNode *top; } LinkStack;初始化void initLinkStack(LinkStack *stack) { stack-top NULL; }入栈int pushLinkStack(LinkStack *stack, int value) { StackNode *node (StackNode *)malloc(sizeof(StackNode)); if (node NULL) { return 0; } node-data value; node-next stack-top; stack-top node; return 1; }出栈int popLinkStack(LinkStack *stack, int *value) { if (stack-top NULL) { return 0; } StackNode *temp stack-top; *value temp-data; stack-top temp-next; free(temp); return 1; }链栈的考点主要集中在“自顶向下遍历”“逆序输出”“释放整条链”这三个场景。释放整条链时一定要先用临时指针保存下一个节点再 free 当前节点否则会丢失后续节点的地址。5.3 共享栈共享栈是一种比较简单的空间优化手段两个栈共用同一个大数组一个栈从数组底部向中间增长另一个栈从数组末尾向中间增长。当两个栈顶指针相遇时整个共享栈满。typedef struct { int data[MAX_STACK_SIZE]; int top1; int top2; } SharedStack; void initSharedStack(SharedStack *stack) { stack-top1 -1; stack-top2 MAX_STACK_SIZE; } int isSharedStackFull(SharedStack *stack) { return stack-top1 1 stack-top2; }共享栈的考点在考研题中出现频率不低主要考“什么时候判满”和“如何确定两个栈各自的空间”。这两个问题本质上都是在问同一个条件两个栈顶指针相向而行中间没有空位时栈满。5.4 栈的经典应用括号匹配括号匹配是栈最典型的应用。思路遍历字符串遇到左括号入栈遇到右括号时看栈顶是否是对应的左括号如果是就出栈如果不是或者栈空则括号不匹配。int isValidBrackets(const char *expr) { SeqStack stack; initStack(stack); for (int i 0; expr[i] ! \0; i) { char ch expr[i]; if (ch ( || ch [ || ch {) { push(stack, ch); } else if (ch ) || ch ] || ch }) { int topChar; if (!peek(stack, topChar)) { return 0; } if ((ch ) topChar () || (ch ] topChar [) || (ch } topChar {)) { int unused; pop(stack, unused); } else { return 0; } } } int unused; return !peek(stack, unused); }注意这里的push(stack, ch)实际上往整型数组里塞了一个字符因为 C 的字符本质是整型语法上合法。但为了严谨可以把data的类型改成char或者把ch强转成int。在考试写代码时如果你不想因为类型问题失分可以单独写一个char类型的栈。5.5 栈的另一个高频点表达式求值中缀表达式转后缀表达式也是大型题目的常客。核心规则数字直接输出。运算符入栈前先把栈中优先级不低于当前运算符的运算符依次弹出输出。左括号直接入栈右括号弹出直到左括号。这里不展开完整代码只强调一个易错点同优先级运算符是左结合所以碰到同优先级时应该先把栈里的弹出来再把新的入栈。例如表达式a - b - c如果碰到第二个减号时不弹出前一个减号后缀表达式就是abc--这其实等于a - (b - c)结果就错了。正确后缀表达式应该是ab-c-。6. 指针核心考点与代码实现指针在数据结构里不是孤立概念它主要体现在链式结构、动态内存管理和函数参数传递这几个方面。6.1 指针与数组数组名在很多上下文中退化为指向首元素的指针但sizeof运算符下除外。这个点在期末题里非常常见。int arr[] {1, 2, 3, 4, 5}; int *p arr; // p[2] 和 arr[2] 等价 printf(%d\n, p[2]); // *(p 3) 和 arr[3] 等价 printf(%d\n, *(p 3));易错点arr是常量地址不能执行arr但p可以执行p。如果题目里写arr结果一定是编译错误。6.2 指针与单链表链表是数据结构里指针用得最多的地方。很多学生写链表时卡在插入和删除的指针修改顺序上。规律其实很固定插入节点先接后继再改前驱。删除节点先用临时指针保存待删除节点再跨过它最后释放。单链表头插法示例typedef struct ListNode { int val; struct ListNode *next; } ListNode; ListNode *headInsert(ListNode *head, int value) { ListNode *node (ListNode *)malloc(sizeof(ListNode)); if (node NULL) { return head; } node-val value; node-next head; head node; return head; }单链表反转是笔试高频题需要三个指针ListNode *reverseList(ListNode *head) { ListNode *prev NULL; ListNode *current head; while (current ! NULL) { ListNode *next current-next; current-next prev; prev current; current next; } return prev; }这里的核心是在修改current-next前必须先把它的旧值保存到next否则走到下一步时链就断了。很多链表类题目包括“两两交换节点”“删除倒数第 N 个节点”核心都是同一个思想先保存后修改。6.3 二级指针与链表头节点当需要在函数内部修改链表头指针本身时有两种做法函数返回新头指针调用处重新赋值比如上面的reverseList。使用二级指针直接修改传入的指针变量。二级指针写法void insertAtHead(ListNode **head, int value) { ListNode *node (ListNode *)malloc(sizeof(ListNode)); if (node NULL) { return; } node-val value; node-next *head; *head node; }调用时ListNode *head NULL; insertAtHead(head, 10);为什么需要二级指针因为 C 语言函数参数默认是值传递。如果你只传ListNode *head函数里修改head不会影响外面的指针变量。而传入head后函数里可以通过*head修改原指针的值。这个点在考试简答题里也很容易被问到。6.4 指针数组与数组指针这两个概念很容混淆指针数组本质是数组数组元素是指针。声明方式int *arr[5]。数组指针本质是指针指向一个数组。声明方式int (*p)[5]。判断技巧看变量名先和谁结合。int *arr[5]中arr先与[5]结合所以它是数组int (*p)[5]中p先与*结合所以它是指针。指针数组常用于保存字符串集合const char *strArr[] {hello, world, data};这里的每个元素都是一个const char *字符串指针。很多期末题会考这类代码能不能编译、能不能修改其中某个字符结论是数组中每个指针可以重新赋值指向其他字符串但通过const限定的字符串内容不能直接修改。6.5 哈希表存放指针如何释放内存热词里专门提到了“hash存放指针如何释放内存”这是 C/C 内存管理常问的问题。思路如下如果哈希表的 value 是通过malloc动态分配的指针那么在销毁哈希表时需要先遍历每个槽位把value指向的内存释放掉再把节点本身释放最后释放哈希表结构。漏掉任何一步都会造成内存泄漏。示例typedef struct HashNode { int key; void *value; struct HashNode *next; } HashNode; typedef struct { HashNode **buckets; int size; } HashMap; void destroyHashMap(HashMap *map) { for (int i 0; i map-size; i) { HashNode *node map-buckets[i]; while (node ! NULL) { HashNode *next node-next; free(node-value); // 释放动态分配的数据 free(node); // 释放节点本身 node next; } } free(map-buckets); free(map); }这里的顺序是先保存next再释放value然后释放node。如果先释放node再访问node-next和node-value就是未定义行为。6.6 指针常见错误与调试常见的指针错误包括现象可能原因调试方式程序运行时崩溃定位到某一行赋值空指针解引用加if (ptr NULL)判断变量值莫名被修改野指针写入检查指针是否被free后没有置空多次free报错double free每次free后把指针置为NULL内存占用持续上涨内存泄漏使用 Valgrind 或 ASan函数返回后局部数组地址失效返回了局部变量地址改用malloc或静态数组每次做指针类题目时可以强制自己回答三个问题这个指针当前指向哪里如果我不小心丢掉了这个地址谁还能释放这块内存代码里有没有可能出现NULL解引用7. 自建“解题栈 Hub”题目管理与批量自测“解题栈 Hub”在这里指的不是一个商业平台而是一套属于你自己的题目管理目录。把刷过的题按“数据结构考点”组织起来每个题目配套题解、代码、易错点时间久了就是一个非常强的复习仓库。7.1 目录结构建议ds-solution-hub/ ├── README.md ├── scripts/ │ ├── build_and_run.sh │ └── batch_test.sh ├── topics/ │ ├── circular_queue/ │ │ ├── README.md │ │ ├── solution.c │ │ └── main.c │ ├── stack/ │ │ ├── bracket_match.c │ │ ├── expression_convert.c │ │ └── shared_stack.c │ └── pointer/ │ ├── linked_list_insert.c │ ├── linked_list_reverse.c │ └── hashmap_memory.c └── templates/ └── solution_comment_template.c每个题目目录里的README.md可以维护这些字段# 题目循环队列队满和队空判断 ## 考点 - 循环队列 - 取模运算 ## 易错点 - 牺牲一个存储单元后队列最多存 MAX_SIZE - 1 个元素 - rear 和 front 都要用取模移动 ## 解题思路 1. 初始化 rear front 0 2. 判空条件front rear 3. 判满条件(rear 1) % MAX_SIZE front 4. 元素个数(rear - front MAX_SIZE) % MAX_SIZE ## 代码位置 solution.c7.2 批量自测脚本写一个小脚本把目录下所有 C 文件编译并运行可以快速验证每个题目是否通过基本用例。这个脚本适合在 Linux 或 macOS 的终端里运行。#!/bin/bash # scripts/build_and_run.sh set -e TOPIC_DIR../topics OUTPUT_DIR../build mkdir -p $OUTPUT_DIR for c_file in $TOPIC_DIR/*/*.c; do if [[ $c_file *main.c ]]; then echo 编译 $c_file gcc -stdc11 -Wall -Wextra -g -o $OUTPUT_DIR/$(basename $c_file .c) $c_file echo 运行 $(basename $c_file .c) $OUTPUT_DIR/$(basename $c_file .c) fi done注意这个脚本默认只编译main.c。如果你的测试代码写在solution.c里可以单独调整编译命令。7.3 对接在线评测系统数据结构刷题通常会用到 OJ 系统。每个 OJ 的输入输出格式不完全一样但通用流程是在本地写好main函数定义好输入读取逻辑。先跑官方的样例输入看输出是否一致。再跑边界用例比如空队列、容量为 1 的循环队列、栈满状态。通过后再提交到 OJ。一个典型的 OJ 风格 C 语言程序框架#include stdio.h int main() { int n; scanf(%d, n); // 根据题目要求实现逻辑 printf(%d\n, result); return 0; }这里特别提醒OJ 上千万不能把整个CircularQueue和SeqStack定义全打印出来也不要在提交代码里加无关的输出。严格按题目要求的输出格式来。8. 时空复杂度分析与性能观察数据结构的很多题目会要求你分析空间复杂度和时间复杂度这也是“资源占用”在数据结构里的对应概念。8.1 循环队列入队O(1)因为只需要一次写入和一次取模。出队O(1)因为只需要一次读取和一次取模。空间复杂度O(n)n是数组容量。判断元素个数O(1)直接用公式不需要遍历。常见考试陷阱如果题目要求“统计队列元素个数”并且不给size字段不能写O(n)的遍历版本正确公式是(rear - front MAX_SIZE) % MAX_SIZE。8.2 栈顺序栈入栈出栈O(1)。链栈入栈出栈O(1)前提是已经知道栈顶指针位置。共享栈入栈出栈O(1)只要不越界。括号匹配时间复杂度O(n)空间复杂度O(n)。8.3 链表相关头插法建立链表每个节点插入O(1)总时间O(n)。链表反转只遍历一遍时间O(n)空间O(1)。指针数组遍历时间O(n)空间取决于存储内容。8.4 如何观察内存问题本地调试时可以用 Valgrind 检查内存泄漏valgrind --leak-checkfull ./a.out如果程序里有申请了但没释放的节点Valgrind 会明确报告“definitely lost”和对应调用栈。这是检查链表、哈希表、栈等动态结构代码是否合格的必备工具。Windows 下可以使用 Visual Studio 的 CRT 内存泄漏检测或者直接安装 WSL 后在 Linux 环境下跑 Valgrind。9. 常见问题与排查方法问题现象可能原因排查方式解决方案循环队列元素个数算错rear - front直接输出没取模用公式(rear - front MAX_SIZE) % MAX_SIZE先算差值加容量再取模队列明明没满却输出队满入队后rear没有取模检查所有rear是否改成(rear 1) % MAX_SIZE统一使用取模移动栈出栈后栈顶值不对top指向约定混乱确定top是栈顶元素下标还是下一个空位下标统一一套写法如果top -1表示空栈则data[top] value入栈链表反转时断链没有先保存current-next在current-next prev前打点输出三个指针轮转法修正释放链表后程序崩溃释放时没有保存next使用 Valgrind 定位先next node-next再free(node)哈希表销毁后内存泄漏只释放了节点没释放 value查看 Valgrind 报告先释放value再释放node编译报错assignment to expression with array type尝试给数组名赋值检查是否有arr ...或arr使用指针变量代替数组名移动free后崩溃野指针或重复释放在释放后置空free(ptr); ptr NULL;10. 最佳实践与学习建议10.1 刷题策略第一遍刷题时先不要追求数量把每个代表性题目吃透。比如循环队列只需要掌握“判空判满、入队出队、元素个数”这三个能力栈只要掌握“顺序栈、链栈、共享栈、括号匹配、表达式转换”这几件事指针只需要掌握“数组与指针的关系、链表插入删除、二级指针、内存释放”。每个题目要求自己用一套固定步骤走完读题标注输入范围和边界。确定数据结构选型。先进先出用队列后进先出用栈频繁中间插入删除考虑链表。写核心操作先写空和满的边界条件。运行样例。刻意测试边界容量为 0 的队列、只有一个元素的栈、空链表。10.2 代码规范写数据结构题目时养成好习惯每个函数要么说明返回值意义要么定义清晰的常量表示成功失败。指针参数用const修饰时说明不会通过该指针修改内容。动态内存申请后立刻检查是否为空。每次free后把指针置为NULL。示例int dequeue(CircularQueue *queue, int *value) { if (queue NULL || value NULL) { return 0; } if (isEmpty(queue)) { return 0; } *value queue-data[queue-front]; queue-front (queue-front 1) % MAX_SIZE; return 1; }10.3 建立自己的“解题栈 Hub”解题栈 Hub 的核心价值是“让每次刷题留下可检索的记录”。建议每个知识点至少沉淀三样东西题目原型一句话描述题目要求避免后期看到题目都忘了考什么。核心代码提取最核心的操作不粘贴整段冗余代码。易错清单把自己犯过的错写进去。考前翻这个比从头刷题高效得多。例如# 循环队列核心代码归档 ## 判空 front rear ## 判满 (rear 1) % MAX_SIZE front ## 入队 data[rear] value; rear (rear 1) % MAX_SIZE; ## 出队 *value data[front]; front (front 1) % MAX_SIZE; ## 易错点 1. 容量 6 的队列最多只能存 5 个元素 2. rear 和 front 的移动必须取模 3. 打印状态时不要忘记加括号10.4 合规与版权提示如果你正在整理自己的解题栈 Hub收录题目时注意题源。来自教材的题目可以写清楚出处来自公开 OJ 的题目尽量给出原题链接。对于考研真题和期末试卷不要大段拷贝到公开博客上尤其是标注了版权说明的资料。个人复习使用没有限制但公开发布时要遵守资源平台规则。11. 总结与下一步循环队列、栈、指针这三个模块是数据结构里最值得花时间去手写实现的内容。循环队列考的是“取模思维方式”栈考的是“记住后进先出的约束条件”指针考的是“内存地址变化的过程”。三者在卷面上经常组合出现但在代码实现上各自独立建议先分别验证再合起来做综合题。最应该先弄明白的三个点循环队列为什么牺牲一个存储单元、栈顶指针top的约定到底怎么统一、链表插入删改时为什么要先保存后继节点。这三个点想通了大部分相关题目都能迁移。最容易踩的坑也集中在这三个点上取模运算丢失、top指针对应关系混乱、释放内存时丢失下一节点地址。把这些写进自己的调试清单下次做这类题时可以先扫一遍。如果这篇文章里的代码你已经能不看答案写出来下一步可以往这两个方向继续扩展一个是栈和队列的相互模拟比如用两个栈实现队列、用两个队列实现栈另一个是把循环队列扩展到循环双端队列理解front和rear在两端插入删除时的移动规则。这两个方向是考研和面试里经常出现的升级考点也是在现有代码框架基础上继续加深理解的好选择。