数据结构第 3 章:栈与队列——受限的线性表反而更有用

写于 2026-11-01

栈和队列在逻辑上仍然是线性表,只是限制了插入删除的位置:栈只允许在一端进出,队列只允许一端进另一端出。这个限制看起来是"能力变弱",实际上是"语义变强"——正因为限制了,才能精确表达"后进先出"和"先进先出"这两种最常见的现实需求,也才能让复杂度降下来。

一、栈:后进先出(LIFO)

只允许在栈顶插入(push)和删除(pop),栈底固定。n 个不同元素入栈,可能的出栈序列有 C(2n,n)/(n+1) 种(卡特兰数),这个结论在选择题里反复出现。

顺序栈:用数组加一个 top 指针。约定 top 指向栈顶元素时,初始 top = −1,判空 top == -1,判满 top == MaxSize-1;约定 top 指向栈顶的下一个空位时,初始 top = 0,判空 top == 0,判满 top == MaxSize。两种约定写法不同但都对,考试要看清题目用的是哪一种。

链栈:用单链表实现,插入删除都在表头做,所以不需要头结点,push 就是头插。链栈不存在栈满问题(除内存耗尽)。

共享栈:两个栈共用一个数组,分别从两端向中间长。栈满条件是 top1 + 1 == top2。这样能把空间利用率提高一倍,适合两个栈此消彼长的场景。

二、栈的应用:为什么每个程序员都要懂栈

  1. 括号匹配:遇左括号入栈,遇右括号出栈比对。结束时栈空才算匹配。O(n)。
  2. 表达式求值:先把中缀表达式转成后缀(逆波兰)表达式,再用栈求值。转后缀时用栈暂存运算符,按优先级出栈。这是编译原理里表达式分析的雏形。
  3. 递归:递归本质就是系统用函数调用栈保存现场。汉诺塔是最典型的例子——n 个盘子要移动 2ⁿ−1 次,递归式是 T(n) = 2T(n−1) + 1,解得 O(2ⁿ)。递归深度就是栈的深度,深度过大会栈溢出,这也是为什么递归要能改写成非递归(显式用栈)。
  4. 函数调用、撤销操作、浏览器后退:都是栈的语义。

三、队列:先进先出(FIFO)

只允许队尾入队、队头出队。

顺序队列的假溢出:用数组加 front/rear 指针时,随着出队 front 前移,前面空出来的位置无法再利用,rear 到数组末尾就"溢出"了——但数组其实没满。解决办法是把它变成循环队列。

循环队列的四个关键公式(用"牺牲一个单元"的经典约定,MaxSize = m):

  • 初始:front = rear = 0
  • 判空:front == rear
  • 判满:(rear + 1) % m == front
  • 元素个数:(rear − front + m) % m

入队:rear = (rear+1) % m 后存值;出队:取值后 front = (front+1) % m。

如果不愿意牺牲那个单元,就要另外用一个计数器 count 或标志位 tag 来区分队空队满——"队空和队满时 front 都等于 rear"是这里唯一的歧义来源,所有方案都是为了消掉这个歧义。

链队列:需要队头指针和队尾指针,通常带一个头结点。出队是删头结点的后继,入队是尾插,都是 O(1)。链队列不会满(除内存耗尽)。

双端队列(deque):两端都能进出,是很多算法(滑动窗口最值)的基础。

四、队列的经典应用

  • 素数环:把 1~n 排成一个环使相邻两数之和为素数。用回溯 + 队列/递归枚举,是"搜索类问题"的入门例子。
  • 停车场管理:车辆进场入队、离场时让出通道上的车进临时栈再复原,是"栈 + 队列配合"的经典建模题。
  • BFS、任务调度、缓冲区:后面第 6 章图的广度优先遍历就是队列驱动的。

易错点与选择题高频点

  1. 判断"某个出栈序列是否合法":用模拟法,按入栈顺序压入,随时比对出栈序列。
  2. 循环队列元素个数公式 (rear − front + m) % m,别写成 rear − front。
  3. 共享栈判满 top1 + 1 == top2。
  4. 递归与栈的关系:递归一定可以用栈改写为非递归,但改写后代码可读性通常下降。
  5. 队列判空判满在循环队列和链队列里完全不同,链队列判空是 front == rear,且没有判满。

这一章在整门课里的位置

第 2 章的链表是本章的实现基础(链栈、链队列都是单链表)。本章的栈和队列又是第 5 章树遍历(递归/栈)、第 6 章图遍历(DFS 用栈、BFS 用队列)的工具。遍历算法的差异,本质就是"用栈还是用队列"的差异——这句话在第 6 章会变得非常具体。

补充:四种实现的复杂度与选型对照

栈和队列都是"操作受限的线性表",所以实现方式就是第 2 章的两种存储方式各来一遍。选型的判断标准只有一条:长度是否可预知、是否要求严格 O(1) 且无额外指针开销。

实现 入/出复杂度 空间 判满条件 适用
顺序栈 O(1) 预分配,可能溢出 top == MaxSize-1 深度可预估(如表达式求值)
链栈 O(1) 每结点一个指针开销 无(除内存耗尽) 深度不可预估、频繁进出
循环队列 O(1) 预分配 (rear+1)%m == front 固定缓冲、生产者消费者
链队列 O(1) 每结点一个指针开销 无 长度变化大、任务调度

一个容易被忽略的区别:顺序栈和循环队列的"满"是必须处理的边界,而链式实现永远没有满的问题——这也是为什么嵌入式与高并发场景更倾向链式(不会因为缓冲区满而丢数据,只是内存吃紧)。

另外要记住:栈和队列的插入删除都只在端点进行,所以都没有"按位置插入"的开销,平均复杂度都是 O(1)。这一点和线性表完全不同,也是它们能被用作底层结构(递归、BFS)的原因。

本篇是《数据结构与算法(93 讲)》第 3 章的知识点摘要。完整板书与代码见 课程页;上一章 线性表,下一章 串与数组。

相关课程

这篇笔记涉及的主题,我录过对应的视频课程: