栈(Stack)
一、栈的定义与特性
定义
栈是限定仅在表尾进行插入和删除操作的线性表。
特性
-
先进后出(FILO - First In Last Out)
-
后进先出(LIFO - Last In First First)
核心概念
-
栈顶(Top):允许操作的一端
-
栈底(Bottom):不允许操作的一端
-
入栈(Push):向栈顶添加元素
-
出栈(Pop):从栈顶移除元素
二、两种"栈"的区别与联系
数据结构中的栈
// 链式栈示例
typedef struct stacknode {
DATATYPE data;
struct stacknode *next;
} LinkStackNode;
-
内存位置:堆空间(动态分配)
-
适用场景:更广泛,可自定义大小
-
操作方式:通过指针操作
系统栈(调用栈)
-
内存位置:0~3G内存中的一段(通常约8M)
-
存储内容:
-
函数的调用关系
-
局部变量
-
参数
-
返回地址
- 工作原理:同样遵循先进后出原则
共同点
-
都遵循先进后出原则
-
都有入栈(push)和出栈(pop)操作
三、栈的存储方式
1. 顺序存储(数组实现)
-
使用连续内存空间
-
需要预分配固定大小
-
操作简单,效率高
2. 链式存储(链表实现)
// 链栈节点结构
typedef struct stacknode {
DATATYPE data; // 数据域
struct stacknode *next; // 指针域
} LinkStackNode;
// 链栈结构
typedef struct {
LinkStackNode *top; // 栈顶指针
int clen; // 栈长度
} LinkStack;
四、栈的分类(按增长方向)
根据栈指针移动方向,栈可分为4种类型:
|-----|---------|----------|------------|
| 类型 | 名称 | Top指针指向 | 新增元素后Top变化 |
| 空增栈 | 空栈+地址增加 | 新元素待插入位置 | 地址增大 |
| 空减栈 | 空栈+地址减少 | 新元素待插入位置 | 地址减小 |
| 满增栈 | 满栈+地址增加 | 最后入栈的元素 | 地址增大 |
| 满减栈 | 满栈+地址减少 | 最后入栈的元素 | 地址减小 |
关键区别:
-
增栈:新增元素后,Top指针指向的内存地址慢慢变大
-
减栈:新增元素后,Top指针指向的内存地址慢慢变小
-
空栈:Top指针指向新元素待插入的位置
-
满栈:Top指针指向最后入栈的元素的位置
五、栈的基本操作接口
链栈操作函数
// 创建栈
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);
六、栈的应用场景
1. 递归问题
-
函数调用栈本身就是栈的应用
-
保存函数调用现场,确保正确返回
2. 回溯问题
-
深度优先搜索(DFS)
-
路径记录和回退
3. 表达式求值
-
中缀转后缀表达式
-
运算符优先级处理
4. 括号匹配检查
-
检查括号是否成对出现
-
检查嵌套是否正确
5. 浏览器历史记录
-
前进/后退功能
-
页面访问历史管理
七、相关数据结构对比
双向链表 vs 栈
// 双向链表节点(对比)
typedef struct dounode {
DATATYPE data;
struct dounode *prev; // 前驱指针
struct dounode *next; // 后继指针
} DouLinkNode;
// 栈节点(对比)
typedef struct stacknode {
DATATYPE data;
struct stacknode *next; // 只有后继指针
} LinkStackNode;
主要区别:
访问方式:
-
栈:只能从栈顶访问(受限)
-
双向链表:可从任意位置访问(灵活)
操作限制:
-
栈:只能在表尾操作
-
链表:可在任意位置插入删除
实现复杂度:
-
栈:实现简单
-
链表:实现相对复杂
八、栈的适用原则
何时使用栈?
-
需要"撤销"操作时
-
需要记录历史状态时
-
问题具有递归特性时
-
需要反转数据顺序时
-
检查嵌套结构时
栈的优势
-
操作简单:只有push和pop
-
效率高:时间复杂度O(1)
-
内存可控:链栈动态分配,不浪费空间
-
逻辑清晰:先进后出,易于理解
九、实际编程示例
表达式求值栈示例
// 使用两个栈:数字栈和运算符栈
LinkStack* num_stack = CreateLinkStack(); // 数字栈
LinkStack* op_stack = CreateLinkStack(); // 运算符栈
// 扫描表达式
while (表达式未结束) {
if (是数字) {
PushLinkStack(num_stack, &数字);
} else if (是运算符) {
// 处理优先级
while (栈顶运算符优先级 >= 当前运算符) {
// 弹出运算
弹出两个数字和一个运算符;
计算结果;
结果入数字栈;
}
PushLinkStack(op_stack, &运算符);
}
}
十、 总结要点
-
栈的本质:受限的线性表,只能在表尾操作
-
核心特性:先进后出(FILO/LIFO)
-
两种实现:顺序存储(数组)和链式存储(链表)
-
四种分类:空增栈、空减栈、满增栈、满减栈
-
应用广泛:递归、回溯、表达式求值、括号匹配等
-
系统对比:数据结构栈(堆空间) vs 系统栈(内存固定区域)