一、栈(Stack):先进后出的线性表
1.1 栈的核心定义与特性
- 定义 :栈是限定仅在表尾(栈顶) 进行插入和删除操作的线性表,另一端(栈底)不允许任何操作。
- 核心特性:先进后出(FILO)/ 后进先出(LIFO)。
- 关键术语 :
- 栈顶:允许插入(入栈)、删除(出栈)的一端;
- 栈底:固定的、不允许操作的一端;
- 入栈(Push):向栈顶添加元素;
- 出栈(Pop):从栈顶移除元素。
1.2 栈的分类与存储方式
(1)按存储方式分类
| 存储方式 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 顺序存储(数组栈) | 基于数组实现 | 访问速度快、实现简单 | 容量固定,易溢出 |
| 链式存储(链栈) | 基于链表实现 | 容量动态扩展,无溢出风险 | 额外的指针内存开销 |
(2)按栈顶指针行为分类(顺序栈特有)
栈的指针行为直接影响元素存储逻辑,是面试 / 开发中的高频考点:
- 空增栈 :top 指针指向新元素待插入的位置(空栈时 top=0),入栈时先存元素再
top++; - 空减栈 :top 指针指向新元素待插入的位置,入栈时先
top--再存元素; - 满增栈 :top 指针指向最后入栈的元素(空栈时 top=-1),入栈时先
top++再存元素; - 满减栈 :top 指针指向最后入栈的元素,入栈时先存元素再
top--。
核心区别:
- 空栈:top 指向 "待插入位置";
- 满栈:top 指向 "最后一个元素位置";
- 增栈:入栈后 top 地址变大;
- 减栈:入栈后 top 地址变小。
1.3 系统栈 vs 数据结构栈
很多开发者会混淆 "系统栈" 和 "数据结构栈",二者原理一致但应用场景不同:
| 维度 | 系统栈 | 数据结构栈 |
|---|---|---|
| 内存位置 | 进程地址空间 0~3G(默认 8M) | 堆空间(malloc 动态分配) |
| 存储内容 | 函数调用关系、局部变量、参数、返回地址 | 自定义业务数据 |
| 管理方式 | 操作系统自动管理 | 开发者手动管理(创建 / 销毁) |
| 适用场景 | 函数调用、递归执行 | 算法(回溯、表达式计算)、业务逻辑 |
1.4 链栈的完整实现(LinkStack.h)
基于链表实现的栈(链式存储),支持动态扩容,无溢出风险:
c
运行
#ifndef _LINKSTACK_H_
#define _LINKSTACK_H_
// 自定义业务数据类型(以坐标/字符为例)
typedef struct person
{
char c;
int row;
int col;
} DATATYPE;
// 栈节点结构
typedef struct stacknode
{
DATATYPE data; // 节点数据
struct stacknode *next; // 指向下一节点(栈底方向)
} LinkStackNode;
// 栈管理结构
typedef struct
{
LinkStackNode *top; // 栈顶指针
int clen; // 栈中有效元素数
} LinkStack;
// 核心操作函数声明
LinkStack* CreateLinkStack(); // 创建栈
int PushLinkStack(LinkStack*ls,DATATYPE*newdata); // 入栈
int PopLinkStack(LinkStack*ls); // 出栈
DATATYPE* GetTopLinkStack(LinkStack*ls);// 获取栈顶元素(不弹出)
int GetSizeLinkStack(LinkStack*ls); // 获取栈大小
int IsEmptyLinkStack(LinkStack*ls); // 判断栈是否为空
int DestroyLinkStack(LinkStack*ls); // 销毁栈
#endif
1.5 栈的典型应用场景
- 递归调用(系统栈自动实现);
- 回溯算法(如迷宫求解、全排列);
- 表达式计算(如四则运算、括号匹配);
- 编辑器撤销 / 重做功能;
- 浏览器前进 / 后退功能;
- 优先级相关问题(如运算符优先级处理)。
二、队列(Queue):先进先出的线性表
2.1 队列的核心定义与特性
- 定义:队列是只允许在一端(队尾)插入、另一端(队头)删除的线性表。
- 核心特性:先进先出(FIFO)。
- 关键术语 :
- 队头(Head):允许删除的一端;
- 队尾(Tail):允许插入的一端;
- 入队(EnQueue):向队尾添加元素;
- 出队(DeQueue):从队头移除元素。
- 核心应用:解决 "速度不匹配" 问题(如缓冲区、消息队列、任务调度)。
2.2 队列的分类
| 类型 | 实现方式 | 核心特点 |
|---|---|---|
| 顺序队列 | 基于数组实现 | 简单但易出现 "假溢出" |
| 循环队列 | 基于数组 + 取余运算 | 解决假溢出,空间利用率高 |
| 链式队列 | 基于链表实现 | 容量动态扩展,无溢出风险 |
2.3 循环队列:解决顺序队列的假溢出问题
(1)假溢出问题
顺序队列的队头(head)和队尾(tail)均向后递增,当 tail 到达数组末尾时,即使数组前端有空余空间,也无法继续入队,这种现象称为 "假溢出"。
(2)循环队列的核心原理
通过对 head/tail 做 "队列总大小取余",让指针在数组中循环移动,核心规则:
- 队空条件:
head == tail; - 队满条件:
(tail + 1) % tlen == head(牺牲一个位置,避免队空 / 队满歧义); - 入队:
tail = (tail + 1) % tlen; - 出队:
head = (head + 1) % tlen。
(3)循环队列定义(SeqQueue)
c
运行
typedef int DATATYPE; // 自定义数据类型
typedef struct queue {
DATATYPE *ptr; // 队列数组指针
int tlen; // 队列总容量
int head; // 队头指针
int tail; // 队尾指针
}SeqQueue;
// 核心操作函数声明
int DestroySeqQueue(SeqQueue *queue); // 销毁队列
DATATYPE QuitSeqQueue(SeqQueue *queue); // 出队
int EnterSeqQueue(SeqQueue *queue, DATATYPE data); // 入队
int IsEmptySeqQueue(SeqQueue *queue); // 判断队空
int IsFullSeqQueue(SeqQueue *queue); // 判断队满
SeqQueue *CreateSeqQueue(int len); // 创建队列
2.4 链式队列:动态扩容的队列实现
基于链表的队列(链式存储),无需考虑容量限制,适合数据量不固定的场景:
c
运行
// 自定义业务数据类型(以学生信息为例)
typedef struct person
{
char name[32];
char sex;
int age;
int score;
} DATATYPE;
// 队列节点结构
typedef struct quenode
{
DATATYPE data; // 节点数据
struct quenode *next; // 指向下一节点
} LinkQueNode;
// 队列管理结构
typedef struct
{
LinkQueNode *head; // 队头指针
LinkQueNode *tail; // 队尾指针
int clen; // 有效元素数
} LinkQue;
// 核心操作函数声明
LinkQue * CreateLinkQue(); // 创建队列
int EnterLinkQue(LinkQue *lq,DATATYPE* newnode); // 入队
int QuitLinkQue(LinkQue*lq); // 出队
DATATYPE* GetHeadLinkQue(LinkQue *lq); // 获取队头元素
int GetSizeLinkQue(LinkQue *lq); // 获取队列大小
int IsEmptyLinkQue(LinkQue *lq); // 判断队空
int DestroyLinkQue(LinkQue * lq); // 销毁队列
2.5 队列的典型应用场景
- 消息队列(如 MQ、异步任务处理);
- 缓冲区(如网络通信的数据包缓冲);
- 任务调度(如线程池、进程调度);
- 广度优先搜索(BFS)算法;
- 打印机任务队列、售票排队系统。
三、栈与队列的核心区别
| 维度 | 栈 | 队列 |
|---|---|---|
| 操作端 | 仅栈顶允许插入 / 删除 | 队尾插入、队头删除 |
| 核心特性 | 先进后出(FILO) | 先进先出(FIFO) |
| 存储优化 | 链栈无溢出,顺序栈有容量限制 | 循环队列解决顺序队列假溢出 |
| 典型应用 | 递归、回溯、表达式计算 | 缓冲、调度、BFS 算法 |
| 指针行为 | 仅维护栈顶指针 | 循环队列维护 head/tail,链式队列维护 head/tail |
四、开发注意事项
4.1 栈的开发要点
- 链栈需注意内存泄漏:出栈 / 销毁时必须释放节点内存;
- 顺序栈需处理栈满 / 栈空异常,避免数组越界;
- 栈顶指针的行为(空增 / 满增)需提前定义,避免逻辑混乱。
4.2 队列的开发要点
- 循环队列的队满 / 队空条件需严格遵循
(tail+1)%tlen == head和head==tail; - 链式队列入队时需更新队尾指针,出队时需处理 "队空" 和 "只剩一个节点" 的边界;
- 队列销毁时需遍历释放所有节点,避免内存泄漏。