news 2026/9/28 6:34:49

栈和队列深度剖析:从原理到工程应用,一篇讲透核心细节

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
栈和队列深度剖析:从原理到工程应用,一篇讲透核心细节

栈和队列这两块内容,在数据结构初阶里属于"看着简单,但坑特别多"的部分。很多初学者把定义背得滚瓜烂熟——栈是后进先出,队列是先进先出——但一写代码就露馅:栈顶指针到底先加一再赋值还是先赋值再加一?循环队列队满条件为什么要牺牲一个存储单元?链式队列的尾指针在出队时怎么处理?这篇笔记就是把这些细节彻底掰开揉碎,从逻辑结构讲到代码实现,再到考试和面试里最常见的应用场景,一次性把栈和队列讲透。适合正在学数据结构的学生、准备考研408的、以及自学编程想补基础的朋友。

1. 栈和队列到底在解决什么问题

1.1 从生活场景理解两种受限线性表

栈和队列本质上都是线性表,但它们是"受了限制"的线性表。普通线性表可以在任意位置插入和删除,栈和队列只能在固定的端点操作。这种限制不是缺点,反而是一种设计上的简化——当业务逻辑本身就只需要"后进先出"或"先进先出"的语义时,用受限线性表能让代码更清晰、更不容易出错。

栈对应生活里最常见的场景就是摞盘子。你洗完一个盘子摞上去,用的时候从最上面拿。后放上去的盘子先被拿走,这就是后进先出(LIFO,Last In First Out)。队列对应的是食堂排队打饭,先来的人先打到饭先走,这是先进先出(FIFO,First In First Out)。

计算机世界里这两个规则无处不在。函数调用用栈,因为函数执行完要回到调用它的地方,最后调用的函数最先执行完;打印任务用队列,因为先提交的任务应该先被处理。理解了这两个生活场景,就理解了栈和队列的核心语义,剩下的全是实现细节。

1.2 为什么栈和队列常常成对出现

如果只学理论不写代码,很难理解为什么栈和队列总是一起讲。我的体会是:它们恰好覆盖了计算机系统里两种最基础的调度模型——时间上最近优先(栈)和空间上到达顺序优先(队列)。

拿操作系统举例。进程之间的函数调用链是栈结构:A调用B,B调用C,C执行完返回B,B执行完返回A,层层回溯。而CPU的任务调度、I/O请求缓冲则是队列结构:请求先到先处理。一个偏向"回溯",一个偏向"顺序",两者互补,构成了计算机系统调度逻辑的两大基石。

从数据结构教学的角度,栈和队列又是理解"物理存储"和"逻辑结构"关系的最佳案例。同一个逻辑结构(栈或队列),既可以用顺序存储(数组)实现,也可以用链式存储(链表)实现,而且两种实现各有优劣。把这两个结构的四种实现吃透,后面学树、图、哈希表都会轻松很多。

1.3 初阶阶段掌握到什么程度才算过关

我给一个明确的验收标准,你可以拿来自测:

  • 能默写顺序栈和链栈的初始化、入栈、出栈、判空、判满代码
  • 能默写循环队列的初始化、入队、出队、判空、判满代码,并清楚为什么浪费一个空间
  • 能画出链式队列在入队和出队时指针的变化过程
  • 能用手工模拟括号匹配和表达式求值的完整过程
  • 知道栈在函数调用和递归中的角色,知道队列在消息缓冲和任务调度中的角色

这五条都做到了,初阶阶段的栈和队列就算过关了。接下来进入正题,一个一个拆。

2. 栈的实现细节与实操要点

2.1 顺序栈:核心结构与前三个关键问题

顺序栈用数组实现,结构体里两个成员:一个数组存数据,一个整型变量top当栈顶指针。这是最标准的写法,408和各大教材都这么定义:

#define MaxSize 10 typedef struct { int data[MaxSize]; int top; } SqStack;

这里第一个关键问题是top初始值。有人初始化为-1,有人初始化为0,两种写法对应的判空、入栈、出栈条件全都不一样。

top = -1的写法:栈空条件是top == -1,栈满条件是top == MaxSize-1。入栈操作:先判断栈满,然后top++,再写入data[top]。出栈操作:先判断栈空,然后取出data[top],再top--。

// 初始化 void InitStack(SqStack *s) { s->top = -1; } // 入栈 int Push(SqStack *s, int x) { if (s->top == MaxSize - 1) // 栈满 return 0; s->top++; s->data[s->top] = x; return 1; } // 出栈 int Pop(SqStack *s, int *x) { if (s->top == -1) // 栈空 return 0; *x = s->data[s->top]; s->top--; return 1; }

top = 0的写法:栈空条件是top == 0,栈满条件是top == MaxSize。入栈:先判断栈满,然后data[top] = x,再top++。出栈:先判断栈空,然后top--,再取出data[top]。

第二种写法其实更符合C语言数组下标从0开始的习惯,但考408和绝大多数教材默认第一种写法。我建议你考试用哪种教材就按哪种来,别混。我自己写代码习惯top = -1写法,因为判断条件更直观。

第二个关键问题是栈满如何处理。顺序栈的致命缺点就是容量固定,栈满之后不能再插入。初阶阶段默认栈满就报错返回,不处理扩容。实际工程里会用动态数组扩容,但数据结构考试不考这个,知道有这回事就行。

第三个关键问题是栈底在哪。栈底固定在数组下标0的位置,栈顶随着入栈出栈在数组中间移动。注意,栈的"底"是固定端,"顶"是活动端。出栈入栈都发生在栈顶,这是栈的核心约束。

2.2 链栈:不需要判满的实现方案

链栈用单链表实现,头结点方向就是栈顶方向。入栈相当于单链表的头插法,出栈相当于删除头结点后的第一个结点。链栈最大的好处是不用判满——只要内存够,就能无限入栈。

typedef struct LinkNode { int data; struct LinkNode *next; } LinkNode, *LinkStack; // 入栈(头插法) int Push(LinkStack *top, int x) { LinkNode *node = (LinkNode *)malloc(sizeof(LinkNode)); if (node == NULL) return 0; node->data = x; node->next = *top; *top = node; return 1; } // 出栈 int Pop(LinkStack *top, int *x) { if (*top == NULL) return 0; // 栈空 LinkNode *node = *top; *x = node->data; *top = node->next; free(node); return 1; }

链栈的判空条件是top == NULL,没有判满操作。每次入栈malloc一次,出栈free一次。注意初始化的时候top = NULL,而不是分配一个头结点——链栈不需要头结点,top直接指向第一个元素结点。

我见过不少初学者在这纠结:链表不是都有头结点吗?为什么链栈可以不用?因为链栈只在一端操作,头结点的作用是统一插入删除的代码逻辑,但链栈的插入删除都在栈顶,不需要头结点来统一逻辑。直接用top指针指向首结点,代码反而更干净。

2.3 共享栈:两个栈挤一个数组

共享栈是顺序栈的一个变体,考得不多但值得了解。两个栈共用一个数组,一个从下标0往上长,一个从下标MaxSize-1往下长。判空条件:左栈top1 == -1,右栈top2 == MaxSize。判满条件:top1 + 1 == top2。

共享栈的价值在于互相调剂空间。比如一个程序里同时需要两个栈,但两个栈的峰值不会同时到达,用共享栈可以让总空间比分别开两个数组更省。这是典型的空间换时间思想的反面——牺牲一点复杂度,换来空间利用率的提升。

写共享栈最容易犯错的地方是判断栈满:左栈入栈时要判断top1 + 1 == top2,右栈入栈时也要判断top1 + 1 == top2,方向不一样但条件一样。因为两个栈向中间生长,碰头的条件就是left和right相邻。

3. 队列的实现细节与实操要点

3.1 顺序队列的假溢出问题

队列的顺序实现比栈复杂,核心原因是队列在两段操作:队头出队,队尾入队。朴素想法是维护两个指针,front指向队头元素,rear指向队尾元素的下一个位置。初始front = rear = 0,入队时data[rear++] = x,出队时x = data[front++]。

这个朴素实现有个严重问题,叫假溢出。反复入队出队之后,front和rear都在不断增大,rear很快到达MaxSize,此时数组前面空了一大片,但rear == MaxSize,程序认为队满了,无法再入队。空间明明还有,却用不了,这就是假溢出。

解决假溢出的思路有两个方向。一是队满时把数据整体搬到数组头部,这叫数据搬移,但每次搬移都是O(n)操作,效率低。二是把数组首尾相接,逻辑上做成一个环,这就是循环队列。

3.2 循环队列:取模运算与队空队满的三种判断方法

循环队列在逻辑上首尾相连,入队出队操作时对下标取模,让指针在数组范围内循环移动。

#define MaxSize 10 typedef struct { int data[MaxSize]; int front; // 指向队头元素 int rear; // 指向队尾元素的下一个位置 } SqQueue;

入队操作:先判断队满,然后data[rear] = x,rear = (rear + 1) % MaxSize。出队操作:先判断队空,然后x = data[front],front = (front + 1) % MaxSize。

这里最关键的问题是如何区分队空和队满。front == rear既可以是队空(初始状态),也可以是队满(转一圈追上了)。有三种解决方案:

第一种,牺牲一个存储单元。约定rear的下一个位置是front时队满,即(rear + 1) % MaxSize == front。这样队空条件是front == rear,队满条件是(rear + 1) % MaxSize == front。优点是判断简单,缺点是数组只能装MaxSize-1个元素。这是408默认的方案。

第二种,增加一个size成员变量。记录当前元素个数,入队时size++,出队时size--。队空条件是size == 0,队满条件是size == MaxSize。优点是不浪费空间,缺点是每次操作都要维护size。

第三种,增加一个tag标志位。约定tag为0表示上一次操作是出队,tag为1表示上一次操作是入队。队空条件是front == rear && tag == 0,队满条件是front == rear && tag == 1。这个方法考试也会考,但实际用的不如前两种多。

我强烈建议把第一种方法彻底吃透,因为考研和期末考试几乎必考。循环队列的元素个数计算也很常考:元素个数 = (rear - front + MaxSize) % MaxSize。这个公式要想清楚:正常情况下rear比front大,直接相减即可;但rear可能在front左边,加上MaxSize再取模就把差值修正到正确区间。

3.3 链式队列:入队出队的指针变化过程

链式队列用单链表实现,维护两个指针:front指向队头结点,rear指向队尾结点。入队操作在rear端进行,出队操作在front端进行。这里有个容易踩坑的细节:链式队列的front不是头结点,而是直接指向第一个元素结点。

typedef struct LinkNode { int data; struct LinkNode *next; } LinkNode; typedef struct { LinkNode *front; // 队头指针 LinkNode *rear; // 队尾指针 } LinkQueue;

入队操作:新结点链接到rear->next,然后rear移到新结点。出队操作:取front结点数据,front指针移到下一个结点。这里有个特殊情况需要单独处理——当队列只有一个结点时,出队后front和rear都会变成NULL。如果此时只是移动front而不处理rear,rear就成了野指针,后续入队会崩溃。

我画一个简单的出队过程说明这个坑:初始状态front和rear都指向A结点,队里只有A一个元素。执行出队时,先取A的数据,然后front = A->next,即front变成NULL。但此时rear还指向A,A已经被free掉了,rear成了野指针。所以出队代码必须加一个判断:

// 出队 int DeQueue(LinkQueue *q, int *x) { if (q->front == NULL) return 0; // 队空 LinkNode *node = q->front; *x = node->data; q->front = node->next; if (q->rear == node) // 队列中只有一个结点 q->rear = NULL; free(node); return 1; }

注意free之后必须把rear置空,这是链式队列最经典的坑。很多教材细节没讲到,导致自己实现时怎么调都错。第二遍写链式队列的时候,这个问题一定要默写一遍。

链式队列的好处是理论上没有容量上限,不像循环队列有MaxSize限制。坏处是每个元素要额外存一个next指针,内存开销大,并且频繁malloc/free有性能损耗。实际工程里,对性能敏感的队列都用循环队列或环形缓冲区,对灵活性要求高的才用链式队列。

4. 栈的经典应用:从栈帧到表达式求值

4.1 函数调用栈帧的形成与销毁

栈在计算机系统里最核心的应用就是函数调用。每次函数调用,系统在栈上分配一块内存区域,叫栈帧(Stack Frame)。栈帧里保存着局部变量、参数、返回地址、保存的寄存器等内容。

栈帧的形成过程大致是:调用函数把实参压栈,然后把返回地址压栈,再跳转到被调函数的入口;被调函数入口处保存上一个栈帧的基址,移动栈指针,给局部变量分配空间。函数返回时,恢复栈指针和基址指针,从栈里弹出返回地址,跳回去继续执行。

每次函数调用压入一个栈帧,函数返回弹出一个栈帧,这就是后进先出。递归函数为什么容易栈溢出?因为递归没结束之前,每层调用的栈帧都还占着,如果递归深度太大,栈空间就不够了。C语言里一个常见的错误是递归没有出口条件,无限递归直接栈溢出崩溃。

理解了栈帧形成过程,很多问题就豁然开朗。比如局部变量的生命周期为什么只在函数内有效?因为局部变量活在栈帧里,函数一返回栈帧就销毁了。为什么函数参数从右往左入栈?因为在C调用约定下,右往左压栈可以保证第一个参数在栈顶附近,方便可变参数函数的实现。

4.2 backtrace栈回溯:崩溃时如何追踪调用链

backtrace栈回溯是栈在调试领域的经典应用。程序崩溃时,错误信息里打印出一串调用链,从崩溃点一层层回溯到main函数。这个过程就是通过回溯栈帧实现的。

崩溃时系统从当前栈帧出发,根据保存的返回地址和基址指针,一层层往上找调用者。每找到一层,就恢复出一层函数的地址和参数。GDB的bt命令、Linux的backtrace库函数、各种语言的异常堆栈都是这么工作的。

以Linux下C程序为例:

#include <execinfo.h> void print_backtrace() { void *buffer[20]; int size = backtrace(buffer, 20); char **strings = backtrace_symbols(buffer, size); for (int i = 0; i < size; i++) { printf("%s\n", strings[i]); } free(strings); }

注意backtrace需要编译时加上-g选项保留符号信息,否则打印出来的只有地址没有函数名。这就是为什么线上环境排查问题,经常提示需要带符号的二进制才能还原出调用栈。

理解backtrace的原理对排查问题特别有帮助。遇到栈溢出的崩溃日志,第一件事不是看崩溃点,而是看调用链哪一层在不断重复。如果是同一层函数反复出现在调用链里,基本可以断定是递归过深;如果调用链里有明显的异常分支,那就往那个分支查。我在实际排查问题的时候,栈回溯信息比日志正文信息量还大,因为它直接还原了程序执行的完整路径。

4.3 括号匹配与表达式求值

括号匹配是栈的经典入门题,也是很多公司笔试的常客。思路很简单:遍历字符串,遇到左括号入栈,遇到右括号判断栈顶是否匹配——匹配则出栈,不匹配则直接报错。遍历结束后,如果栈不空,说明有左括号没闭合。

int isValid(char *s) { int len = strlen(s); char *stack = (char *)malloc(sizeof(char) * len); int top = -1; for (int i = 0; i < len; i++) { if (s[i] == '(' || s[i] == '[' || s[i] == '{') { stack[++top] = s[i]; } else { if (top == -1) { free(stack); return 0; } // 右括号多了一个 char c = stack[top--]; if ((s[i] == ')' && c != '(') || (s[i] == ']' && c != '[') || (s[i] == '}' && c != '{')) { free(stack); return 0; // 匹配失败 } } } int result = top == -1; free(stack); return result; }

注意这个算法里有个边界条件容易漏:遇到右括号时栈为空说明右括号多余,这直接返回不匹配。很多初学写的版本漏掉这个判断,遇到"])}"之类的输入就报错了。

表达式求值是栈的另一个经典应用场景。中缀表达式"(1+2)*3"对人友好,但计算机处理不方便。通常先转换为后缀表达式"1 2 + 3 *",再用栈求值。

手工模拟中缀转后缀的过程:遇到数字直接输出,遇到运算符与栈顶比较优先级——如果当前运算符优先级不高于栈顶,则弹出栈顶,直到栈为空或栈顶优先级更低,然后当前运算符入栈;遇到左括号直接入栈,遇到右括号弹出直到左括号。转换结束把栈里剩余运算符全部弹出。

后缀表达式求值更简单:遇到数字入栈,遇到运算符弹出两个数字计算,结果入栈。最后栈里只剩一个数字,就是表达式的结果。

这两个算法的代码不长,但手工模拟的过程一定要多画几遍。考试时表达式求值和括号匹配都是必考的模拟题,手算能力比写代码能力更重要——因为考试考的是过程而不是代码。

5. 队列的经典应用:从阻塞队列到消息队列

5.1 线程池里的阻塞队列

队列在实际工程里最常见的角色就是缓冲。当某个组件处理速度跟不上生产速度时,中间加一个队列缓冲,让生产者和消费者解耦,这是计算机系统设计的基本套路。

拿线程池举例。线程池维护若干工作线程和一个任务队列,提交任务时把任务放入队列,工作线程从队列里取任务执行。如果队列满了,提交任务有两种策略:直接拒绝,或者阻塞等待。Java里的ThreadPoolExecutor用阻塞队列来实现这种等待——队列满时提交任务的操作会被阻塞,直到队列有空间。

阻塞队列和非阻塞队列的核心区别在于:非阻塞队列队列满时直接返回失败,阻塞队列会挂起调用方直到可以入队。这个"阻塞"机制就是通过锁和条件变量配合实现的。线程在队列空时等待"有任务"的信号,在队列满时等待"有空位"的信号。

实际选型时,不同的阻塞队列策略对应不同的任务处理语义。比如JDK里的ArrayBlockingQueue是有界的,LinkedBlockingQueue可以是有界也可以是无界的,SynchronousQueue不存储任务而是直接把任务交给线程。选错了队列策略,线程池的行为就和你预期的不一样——比如用无界队列时,任务量暴增会导致队列无限膨胀,内存被撑爆。

这个道理放在操作系统里也成立。CPU的IO请求缓冲、网络数据包的接收缓冲区、打印任务队列,本质上全是队列。队列的FIFO特性天然保证了先来先服务,这就是公平性的基础。

5.2 消息队列选型对比与避坑指南

消息队列是"队列"思想在分布式系统里的扩展。Kafka、RabbitMQ、RocketMQ是三个最常见的开源消息中间件,很多人选型时纠结半天,我的建议是按场景匹配,不要盲目追新。

Kafka的核心优势是高吞吐。它用顺序写磁盘加批量发送的方式,把单机写入能力做到每秒百万级。代价是功能比较基础,消息堆积能力极强,适合日志收集、大数据管道、流处理这类场景。但Kafka在消息不丢失的保证上依赖配置配合,用不好会有丢消息的风险。

RabbitMQ的核心优势是功能完善、路由灵活。基于Erlang的AMQP实现,队列、交换机、绑定关系这套模型非常灵活,各种复杂路由规则都能表达。适合企业内部系统集成、任务分发这类场景。缺点是吞吐量不如Kafka,堆积能力弱,消息一多性能就明显下降。

RocketMQ是阿里开源的消息中间件,定位在Kafka和RabbitMQ之间。支持延迟消息、事务消息、消息重试这些企业级功能,社区活跃度在国内很高。在金融、电商这类对可靠性要求高的场景里用得比较多。

选型避坑指南我可以直接总结成几条经验:

  • 吞吐量要求极高、可以接受功能简单:选Kafka
  • 路由灵活、集成方便、吞吐量要求不高:选RabbitMQ
  • 金融/电商场景、需要事务消息和可靠性保证:选RocketMQ
  • 团队没人用过、只有运维能力一般的人:选最熟悉的那个,而不是最强的那个
  • 消息量现阶段不大但预期会涨:优先选支持水平扩展的,Kafka和RocketMQ都比RabbitMQ好扩展

还有一个最常见的坑是重复消费。消息队列的投递语义通常是至少一次(At-Least-Once),这意味着消费者可能同一消息收到两遍。解决方案是消费端做幂等:用消息里的业务ID去数据库查重,已经处理过就直接跳过。RocketMQ自带消息去重的API,Kafka需要自己在消费端实现,RabbitMQ基于AMQP也可以实现但不那么直接。这些细节在初阶笔记里不用深入,但你要知道队列上到生产环境之后,单纯的数据结构知识是不够的——还需要配合系统设计的思维。

5.3 单调队列优化动态规划

单调队列是队列在算法竞赛和面试题里的进阶玩法,虽然叫"队列",但它维护的是队列内部元素单调递增或单调递减,不是简单的FIFO。

最经典的例子是滑动窗口最大值。给定数组和一个大小为k的窗口,窗口每次向右滑动一个位置,求每个窗口内的最大值。暴力做法是每个窗口内扫一遍,O(nk)。单调队列的优化能做到O(n)。

核心思想:队列里只保留"有可能是当前窗口最大值"的元素下标,且元素对应的值单调递减,队头永远是当前窗口最大值。每次滑动窗口时,先检查队头是否已滑出窗口,滑出就弹出;然后新元素从队尾入队,入队前把所有比新元素小的队尾元素全部弹出——因为它们已经不可能成为最大值了。

// 求数组a长度为n的滑动窗口最大值,窗口大小k int deque[100005], head = 0, tail = -1; for (int i = 0; i < n; i++) { // 队头滑出窗口 if (head <= tail && deque[head] <= i - k) head++; // 弹出队尾比a[i]小的元素 while (head <= tail && a[deque[tail]] <= a[i]) tail--; // 当前元素入队 deque[++tail] = i; // 窗口形成后,队头就是最大值 if (i >= k - 1) printf("%d ", a[deque[head]]); }

单调队列的难点在于理解"弹出比新元素小的元素"这一步。每个元素最多入队出队一次,所以总复杂度O(n)。用在DP优化上,比如1D/1D动态规划里,如果状态转移方程是f[i] = max/min(f[j]) + cost[i],而j的取值区间单调移动,就可以用单调队列把内层循环从O(n)降到O(1)。主站DP优化常考,面试时能说出这个思路是加分项。

初阶阶段不需要掌握所有单调队列的变形题,但滑动窗口最大值这个经典题一定要做到闭眼能写能讲。因为它考察的点很综合:既考队列的应用能力,又考对指针边界的控制能力。

6. 常见问题与排查技巧实录

6.1 栈溢出的三个高发场景

栈溢出(Stack Overflow)是初学者和面试官都绕不开的经典问题。我总结三个高发场景:

第一个是无限递归。递归函数缺少终止条件,或者终止条件永远不满足,导致栈帧不断压入直到栈满。排查方法:看崩溃日志里的调用链,如果某个函数名反复出现,基本就是这里。

第二个是深递归+大局部变量数组。递归深度可能只有几百层,但每层都声明了一个很大的局部数组,每层栈帧占好几KB,栈很快就被耗尽了。排查方法:把数组改成static或移到堆上分配(用malloc),用空间换栈空间。

第三个是过大的局部结构体传值。结构体作为参数按值传递时,会整个拷贝到栈上。如果结构体很大且函数调用频繁,栈开销会非常可观。排查方法:改成传指针。

C语言栈默认大小在Linux下通常是8MB,Windows下是1MB。想临时调大栈空间可以改编译选项,但治标不治本——栈溢出的本质是无限递归或超深递归,正确做法是优化算法把递归改成循环迭代,或者在堆上手动模拟栈。注意:不要轻易在生产代码里依赖栈空间调大,换平台就失效了。

6.2 循环队列长度计算与边界条件

循环队列的边界问题在笔试里特别爱考。核心公式只有一个:元素个数 = (rear - front + MaxSize) % MaxSize。

我举几个例子让你彻底记住。假设MaxSize = 10,front = 3,rear = 7,元素个数 = (7 - 3 + 10) % 10 = 4,正确。假设front = 7,rear = 2,元素个数 = (2 - 7 + 10) % 10 = 5。此时实际存储情况是下标7、8、9、0、1上有元素,确实是5个。

用牺牲一个存储单元的方法判断队空队满时,最容易出错的是队满时rear刚好在MaxSize-1的情况。比如front = 0,rear = 9,MaxSize = 10,此时(rear + 1) % MaxSize = 0 == front,队满。但数组下标0的位置可能没有元素——它被"牺牲"了,永远不存数据。这是正确行为,不是bug。

另一个考点是循环队列里front和rear的初始值不一定都是0。有些实现里rear初始化为MaxSize-1,这时入队操作要先rear = (rear + 1) % MaxSize再存数据。这个变体写法在严蔚敏的教材里出现过,考试时建议先看清题目给的初始化方式,再写代码,不要默认所有循环队列的初始化都相同。

6.3 复试与笔试高频题速查表

整理一份栈和队列的高频考点速查表,考前对照着复习比漫无目的地刷题高效:

考点核心结论
栈和队列的相同点都是操作受限的线性表
栈和队列的不同点栈LIFO,队列FIFO
栈满判断(top=-1写法)top == MaxSize - 1
循环队列队满判断(牺牲空间法)(rear + 1) % MaxSize == front
循环队列元素个数计算(rear - front + MaxSize) % MaxSize
链栈是否有栈满无,只要内存够
链式队列入队操作位置队尾,即rear端
链式队列出队操作位置队头,即front端
链式队列只有一个元素时出队front和rear都要置NULL
栈的应用场景函数调用、括号匹配、表达式求值、DFS、撤销操作
队列的应用场景任务调度、消息缓冲、BFS、打印队列、消息队列中间件
单调队列的典型题滑动窗口最大值,O(n)复杂度
backtrace原理回溯栈帧,展示函数调用链

这些结论看起来简单,但每一条背后都有代码逻辑支撑。我的学习建议是:把每一行结论都回到代码里验证一遍,比如"链式队列出队在队头"这条,动手画一下出队过程图,画一遍就永久记住了。

6.4 初学阶段最常见的四个实现错误

写代码阶段容易翻车的四个点,我逐个讲清楚:

顺序栈先入栈再判满。正确顺序是先判断是否栈满,满了直接返回失败,不能先赋值再判断——那样数据已经写入到越界位置了,等发现栈满时已经晚了。

循环队列入队忘记取模。rear = (rear + 1) % MaxSize,这里的%不能省。直接rear++的话,rear一旦超过MaxSize-1,下标越界。这是循环队列和普通顺序队列的唯一区别,漏掉取模就等于没写循环队列。

链式队列出队时忘了处理队尾指针。前面详细讲过,队列只有一个元素时,出队后rear必须置NULL,否则野指针导致后续入队崩溃。这个错误在初学时几乎必犯,排查方法:单步调试时盯住rear的值。

参数传递用错。所有修改结构体内容的函数,必须传指针。如果你写的是void Push(SqStack s, int x),那函数里修改的只是副本,调用方的栈根本不会变。这个问题在考试的手写代码题里不会暴露(因为只写逻辑),但上机实操时每次都会翻车。

我自己的排错习惯是:先检查传参类型,再检查边界条件,最后看指针移动。这三步走完,八成的问题已经暴露了。剩下两成用gdb单步跟踪,不要瞎改代码试运气——那是最浪费时间的方式。

个人实操体会

栈和队列学到什么程度算真正掌握?我的判断标准是:能不能不看代码,把每种实现的出入操作过程画出来。能画出顺序栈入栈时top从-1变0的过程,能画出循环队列因为取模从下标9转回下标0的过程,能画出链式队列出队后两个指针的移动过程——这个结构基本就刻在脑子里了。

再分享一个备考技巧:栈和队列的内容虽然多,但考试就围着"指针移动"和"边界条件"出题。做模拟题的时候,不管题目怎么变,先画一个空数组,用铅笔标出front和rear或top的每一步变化,正确答案自然就出来了。这个习惯我在复习408时练了无数遍,从来没失手。

最后提醒一点:数据结构是实践学科,光看笔记不写代码永远学不透。栈和队列的代码每一版都很短,加起来不到两百行,全部手写一遍用不了半小时。写完自己设计几个特殊场景测一测——空栈出栈、空队列出队、队列满时入队、链式队列只有一个元素时出队——把这些边界条件测完,考试和面试遇到栈和队列的题,你都稳了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 6:34:39

佛山市网络seo推广公司哪家好?3个避坑点教你选对

佛山市网络seo推广公司哪家好?3个避坑点教你选对 自己不会代码想做网站,最怕找错佛山的网络SEO推广公司。 很多人搜“哪家好”,其实是在问:谁不会忽悠我? 别信那些“保首页”的承诺,先看他们怎么解决SSL证书和备案的坑。 01 证书变更与注销:别让旧数据拖死新站…

作者头像 李华
网站建设 2026/9/28 6:34:24

烽火HG680-KA免短接刷机全攻略:ADB与U盘升级实战详解

烽火HG680-KA这台盒子&#xff0c;说它是刷机圈的“常青树”一点不夸张。我手里前前后后过过好几台&#xff0c;有收来的二手货&#xff0c;原厂系统卡到遥控器按下去要等两三秒才有反应&#xff1b;也有自己刷第三方固件时手滑刷成半砖&#xff0c;最后靠免短接路子救回来的。…

作者头像 李华
网站建设 2026/9/28 6:34:22

3个免费工具揭秘网站开发费用是否资本化的坑

3个免费工具揭秘网站开发费用是否资本化的坑 找建站公司报价单满天飞,心里没底怕被坑高价?别慌。我刚帮一家电商客户砍下40%预算,全靠这3个免费工具核对细节。今天就把【网站开发费用是否资本化】的底层逻辑拆给你看,从财务合规到技术选型,全是血泪换来的干货。 概念速懂:这笔钱到底算资产还是费用…

作者头像 李华
网站建设 2026/9/28 6:33:53

科丰化工东莞网站建设避坑指南:一文搞懂从丑到专业的蜕变

科丰化工东莞网站建设避坑指南:一文搞懂从丑到专业的蜕变 做B2B化工行业的网站,最怕什么?不是客户少,而是客户来了,看一眼首页就走了。很多东莞的化工老板,为了省钱或者赶工期,直接套了个网上下载的模板。结果呢?配色像九十年代的Windows系统,字体小得看不清,产品图模糊得看不出是塑料还是金属。这种“…

作者头像 李华
网站建设 2026/9/28 6:33:46

怎样建立自己网站难吗?最佳实践避坑指南

怎样建立自己网站难吗?最佳实践避坑指南 改个需求建站公司拖一周,后台改个颜色要等三天,这种憋屈感谁懂?很多老板觉得【怎样建立自己网站难吗】这个问题很抽象,其实难的不是写代码,而是难在沟通成本失控和技术黑箱。我干了十年这行,见过太多项目因为缺乏透明的【最佳实践】流程,最后变成无底洞。今天不聊虚的,直接…

作者头像 李华
网站建设 2026/9/28 6:33:39

网站建设微商城多少钱?避坑指南与性能优化实战

网站建设微商城多少钱?避坑指南与性能优化实战 昨晚凌晨三点,手机突然疯狂震动。我迷迷糊糊接起电话,对方声音颤抖:“老板,网站打不开了,浏览器弹出红色警告,说你的站被黑了,挂满了非法广告!” 那一刻,冷汗瞬间湿透后背。你问我为什么?因为我为了省那几千块钱,当初建站时没做好安全加固,也没搞 性能优化…

作者头像 李华