【考研408数据结构-04】 栈与队列:受限的线性表
📚 【考研408数据结构-04】 栈与队列:受限的线性表
🎯 考频:⭐⭐⭐⭐⭐ | 题型:选择题、综合应用题、算法设计题 | 分值:约8-15分
引言
想象你正在自助餐厅取餐盘——你只能从最上面拿走盘子,洗好的盘子也只能放在最上面。再想象你在银行排队——先来的客户先被服务,后来的客户只能排在队尾。这两个日常场景完美诠释了计算机科学中两种最基础却极其重要的数据结构:栈(Stack)和队列(Queue)。
在408考试中,栈与队列是必考重点,近5年真题中平均每年出现2-3道相关题目。它们不仅作为独立考点出现,更是理解递归、表达式求值、图的遍历等高级算法的基础。
本文将帮你彻底掌握栈与队列的精髓,从原理到实现,从基础到应用,让你在考场上游刃有余。
学完本文,你将能够:
- ✅ 深刻理解栈与队列作为"受限线性表"的本质
- ✅ 熟练编写各种栈与队列操作的C语言代码
- ✅ 掌握循环队列的判空判满技巧
- ✅ 快速解答408真题中的相关问题
一、知识精讲
1.1 概念定义
栈(Stack)
栈是一种后进先出(LIFO, Last In First Out)的线性表,它限制了插入和删除操作只能在表的同一端进行。我们把允许操作的一端称为栈顶(top),另一端称为栈底(bottom)。
💡 类比理解:栈就像一个羽毛球筒,你只能从筒口放入或取出羽毛球,最后放入的球总是最先被取出。
408考纲要求:⭐ 掌握
队列(Queue)
队列是一种**先进先出(FIFO, First In First Out)**的线性表,它限制了插入操作只能在表的一端进行(队尾rear),而删除操作只能在另一端进行(队头front)。
💡 类比理解:队列就像排队买票,新来的人在队尾加入,买完票的人从队头离开。
408考纲要求:⭐ 掌握
⚠️ 易混淆点:栈和队列都是线性表的特殊形式,它们的"受限"体现在操作位置的限制,而不是存储结构的限制。两者都可以用顺序存储或链式存储实现。
1.2 原理分析
栈的工作原理
栈的核心操作包括:
- Push(入栈):在栈顶插入元素
- Pop(出栈):删除栈顶元素
- GetTop(取栈顶):获取栈顶元素但不删除
- IsEmpty(判空):判断栈是否为空
示例:计算表达式 3 + 4 * 2
时,操作符栈的变化:
- 遇到
+
,入栈 - 遇到
*
(优先级更高),入栈 - 计算
4 * 2
,*
出栈 - 计算
3 + 8
,+
出栈
队列的工作原理
队列的核心操作包括:
- EnQueue(入队):在队尾插入元素
- DeQueue(出队):删除队头元素
- GetHead(取队头):获取队头元素但不删除
- IsEmpty(判空):判断队列是否为空
循环队列的精髓:为了避免"假溢出"问题,我们使用循环队列。关键在于:
- 队头指针:
front = (front + 1) % MaxSize
- 队尾指针:
rear = (rear + 1) % MaxSize
🎯 重点:循环队列判空判满的三种方法(408必考):
- 牺牲一个存储单元:
(rear + 1) % MaxSize == front
为满 - 设置标志变量 flag
- 设置计数器 count
1.3 性质与特点
特性 | 栈 | 队列 |
---|---|---|
操作特点 | LIFO(后进先出) | FIFO(先进先出) |
插入位置 | 栈顶 | 队尾 |
删除位置 | 栈顶 | 队头 |
典型应用 | 函数调用、表达式求值、括号匹配 | 进程调度、缓冲区、BFS |
时间复杂度 | Push/Pop: O(1) | EnQueue/DeQueue: O(1) |
空间复杂度 | O(n) | O(n) |
优缺点分析:
✅ 栈的优点:
- 操作简单高效
- 天然支持递归
- 内存管理方便
❌ 栈的缺点:
- 只能访问栈顶元素
- 容量受限(顺序栈)
✅ 队列的优点:
- 公平性好(FIFO)
- 适合缓冲和调度
❌ 队列的缺点:
- 顺序队列存在假溢出
- 只能访问队头队尾
二、代码实现
2.1 顺序栈的完整实现
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>#define MAXSIZE 100 // 栈的最大容量// 顺序栈的结构定义
typedef struct {int data[MAXSIZE]; // 存储栈元素的数组int top; // 栈顶指针,指向栈顶元素
} SqStack;// 初始化栈
void InitStack(SqStack *S) {S->top = -1; // 空栈时栈顶指针为-1
}// 判断栈空
bool StackEmpty(SqStack S) {return S.top == -1;
}// 判断栈满
bool StackFull(SqStack S) {return S.top == MAXSIZE - 1;
}// 入栈操作 - 时间复杂度O(1)
bool Push(SqStack *S, int x) {if (StackFull(*S)) {printf("栈满,无法入栈!\n");return false;}S->data[++S->top] = x; // 先移动指针,再存入元素return true;
}// 出栈操作 - 时间复杂度O(1)
bool Pop(SqStack *S, int *x) {if (StackEmpty(*S)) {printf("栈空,无法出栈!\n");return false;}*x = S->data[S->top--]; // 先取出元素,再移动指针return true;
}// 获取栈顶元素
bool GetTop(SqStack S, int *x) {if (StackEmpty(S)) {return false;}*x = S.data[S.top];return true;
}
2.2 循环队列的完整实现(408高频考点)
#define MAXSIZE 100// 循环队列的结构定义
typedef struct {int data[MAXSIZE]; // 存储队列元素的数组int front; // 队头指针int rear; // 队尾指针
} SqQueue;// 初始化队列
void InitQueue(SqQueue *Q) {Q->front = Q->rear = 0; // 初始时队头队尾指向同一位置
}// 判断队空 - 队头队尾指针相等时为空
bool QueueEmpty(SqQueue Q) {return Q.front == Q.rear;
}// 判断队满 - 牺牲一个存储单元的方法
bool QueueFull(SqQueue Q) {return (Q.rear + 1) % MAXSIZE == Q.front;
}// 入队操作 - 时间复杂度O(1)
bool EnQueue(SqQueue *Q, int x) {if (QueueFull(*Q)) {printf("队列满,无法入队!\n");return false;}Q->data[Q->rear] = x; // 在队尾插入元素Q->rear = (Q->rear + 1) % MAXSIZE; // 循环移动队尾指针return true;
}// 出队操作 - 时间复杂度O(1)
bool DeQueue(SqQueue *Q, int *x) {if (QueueEmpty(*Q)) {printf("队列空,无法出队!\n");return false;}*x = Q->data[Q->front]; // 取出队头元素Q->front = (Q->front + 1) % MAXSIZE; // 循环移动队头指针return true;
}// 获取队列长度(元素个数)
int QueueLength(SqQueue Q) {return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
复杂度分析:
- 时间复杂度:所有基本操作均为 O(1)
- 空间复杂度:O(n),n为栈或队列的最大容量
三、图解说明
【图1】栈的操作过程演示
初始状态(空栈):
| |
| | ← top = -1
|_____|底部步骤1:Push(3)
| |
| 3 | ← top = 0
|_____|步骤2:Push(5)
| 5 | ← top = 1
| 3 |
|_____|步骤3:Pop() 返回5
| |
| 3 | ← top = 0
|_____|
【图2】循环队列的关键状态
空队列:front = rear = 0
[ ][ ][ ][ ][ ]↑
front/rear队列有3个元素:
[ ][A][B][C][ ]↑ ↑front rear队列满(牺牲一个单元):
[D][E][ ][A][B][C]↑ ↑rear front
(rear+1)%6 == front
【图3】队空队满判断对比
判断方法 | 队空条件 | 队满条件 | 优点 | 缺点 |
---|---|---|---|---|
牺牲一个单元 | front==rear | (rear+1)%MAX==front | 简单直观 | 浪费空间 |
设置flag | frontrear && flag0 | frontrear && flag1 | 不浪费空间 | 需额外变量 |
计数器count | count==0 | count==MAX | 可快速获取长度 | 需额外变量 |
四、真题演练
【2022年408真题】
题目:若用大小为6的数组来实现循环队列,且当前rear和front的值分别为0和3,当从队列中删除一个元素,再加入两个元素后,rear和front的值分别为多少?
解题思路:
- 初始状态:rear=0, front=3
- 删除一个元素:front = (3+1)%6 = 4
- 加入第一个元素:rear = (0+1)%6 = 1
- 加入第二个元素:rear = (1+1)%6 = 2
答案:rear=2, front=4
⚠️ 易错点:注意循环队列中指针的循环移动,使用取模运算!
【2021年408真题·改编】
题目:栈S和队列Q的初始状态为空,元素a,b,c,d,e,f依次通过栈S,一个元素出栈后即进入队列Q。若所有元素经过栈和队列后,出队列的顺序可能是?
解题思路:
- 元素先经过栈(LIFO),再经过队列(FIFO)
- 关键是确定元素的入栈出栈时机
- 可能的序列需满足栈的约束
举一反三:如果改为"元素可以在栈中停留任意时间",则需要用栈混洗(stack-sortable)的概念来判断。
五、在线练习推荐
LeetCode精选题目
- 🟢 Easy: 20. 有效的括号(栈的经典应用)
- 🟡 Medium: 155. 最小栈(栈的扩展设计)
- 🟡 Medium: 622. 设计循环队列(必做!)
- 🔴 Hard: 84. 柱状图中最大的矩形(单调栈)
练习顺序建议
- 先完成基础题:括号匹配、逆波兰表达式
- 再练习设计题:实现栈、实现队列
- 最后挑战应用题:单调栈、优先队列
推荐在牛客网"408考研专区"完成栈与队列专项练习,共30道题。
六、思维导图
栈与队列:受限的线性表
├── 栈(LIFO)
│ ├── 基本操作
│ │ ├── Push入栈
│ │ ├── Pop出栈
│ │ └── GetTop取栈顶
│ ├── 存储结构
│ │ ├── 顺序栈
│ │ └── 链栈
│ └── 典型应用
│ ├── 括号匹配
│ ├── 表达式求值
│ └── 递归实现
├── 队列(FIFO)
│ ├── 基本操作
│ │ ├── EnQueue入队
│ │ ├── DeQueue出队
│ │ └── GetHead取队头
│ ├── 存储结构
│ │ ├── 顺序队列
│ │ ├── 循环队列⭐
│ │ └── 链队列
│ └── 典型应用
│ ├── 层序遍历
│ ├── BFS
│ └── 缓冲区
└── 特殊结构├── 双端队列├── 优先队列└── 栈队互相实现
七、复习清单
✅ 本章必背知识点清单
概念理解
- 能准确说出栈的LIFO特性含义
- 能准确说出队列的FIFO特性含义
- 理解"受限线性表"的本质
- 掌握循环队列解决假溢出的原理
代码实现
- 能手写顺序栈的Push和Pop操作
- 能手写循环队列的入队出队操作
- 记住循环队列指针移动:
(指针+1) % MAXSIZE
- 掌握三种队空队满判断方法
应用能力
- 会用栈实现括号匹配算法
- 能进行中缀转后缀表达式
- 会分析栈的输出序列合法性
- 掌握循环队列长度计算公式
真题要点
- 记住循环队列判满:
(rear+1)%MAX == front
- 掌握n个元素的出栈序列数:卡特兰数
- 理解共享栈的实现原理
- 记住常见陷阱:循环队列的下标计算必须取模
八、知识拓展
工程实践中的应用
- 操作系统:进程调度使用多级反馈队列
- 编译器:语法分析使用栈进行递归下降分析
- Web服务器:使用队列管理请求,实现负载均衡
- 消息队列:RabbitMQ、Kafka等中间件的核心是队列
常见误区
⚠️ 误区1:认为栈只能用数组实现
- 正解:栈可以用数组(顺序栈)或链表(链栈)实现
⚠️ 误区2:循环队列一定要牺牲一个存储空间
- 正解:这只是一种方法,还可以用flag或count
⚠️ 误区3:队列满时rear一定在front前面
- 正解:循环队列中rear可能在front的任何位置
记忆技巧
🎵 口诀:
- “栈顶进出一个口,后进先出要记熟”
- “队列两端分进出,先进先出按序走”
- “循环队列防假满,取模运算是关键”
结语
栈与队列作为最基础的数据结构,其重要性不言而喻。它们不仅是408考试的必考点,更是理解递归、树的遍历、图的搜索等高级算法的基础。本文从原理到实现,从基础到应用,全方位解析了栈与队列的核心知识。
核心要点回顾:
- 🎯 栈是LIFO结构,队列是FIFO结构
- 🎯 循环队列是解决假溢出的关键技术
- 🎯 队空队满的判断是408的高频考点
- 🎯 栈在表达式求值、递归中应用广泛
- 🎯 队列在BFS、缓冲区管理中不可或缺
栈与队列是通向树和图的桥梁。下一篇文章,我们将深入探讨**《串与KMP算法:模式匹配的艺术》**,揭开字符串匹配的神秘面纱,掌握KMP算法的精髓。
💪 学习建议:栈与队列的代码相对简单,但细节很多。建议你亲手实现一遍所有基本操作,特别是循环队列的各种判断条件。记住,编程能力是408算法设计题的基础,现在打好基础,后续学习会轻松很多!
加油,考研人!下一个上岸的就是你!🚀