# 第四章 栈
## 栈的说明
### 什么是栈
栈是一种用于存储数据的简单数据结构(与链表类似)。数据入栈的次序是栈的关键。可以把自助餐厅中的一堆盘子看作一个栈的例子。当盘子洗干净后,它们会添加到栈的顶端。
当需要盘子时,也是从栈的顶端拿取。所以第一个放入栈中的盘子最后才能被拿取。
- 定义:栈(stack)是一个有序线性表,只能在表的一端(称为栈顶,top)执行插入和删除操作。
- 最后插入的元素将第一个被删除。所以,栈也称为后进先出(Last In First Out,LIFO)或先进后出(First In Last Out,FILO)线性表。
两个改变栈操作都有专用名称。一个称为入栈(push),表示在栈中插入一个元素;
另一个称为出栈(pop),表示从栈中删除一个元素。
试图对一个空栈执行出栈操作称为下溢(underlow);
试图对一个满栈执行人栈操作称为溢出(overflow)。
通常,溢出和下溢均认为是异常。下图是一个栈的例子。

---
## 栈抽象数据类型
**下面给出栈抽象数据类型中的操作。为了简单起见,假设数据类型为整型。**
1. **栈的主要操作**
void push(int data):将data(数据)插入栈。
int pop():删除并返回最后一个插入栈的元素。
2. 栈的辅助操作
int top():返回最后一个插入栈的元素,但不删除。
int size():返回存储在栈中元素的个数。
int isEmpty():判断栈中是否有元素。
int isStackFullo):判断栈中是否存满元素。
## 异常
在执行操作时发生的错误称为异常。当操作不能执行时,会“抛出”异常。
在栈抽象数据类型中,pop操作和top操作在栈空时是不能执行的。
试图对一个空栈执行pop(或top)操作会抛出异常。试图对一个满栈执行push操作也会抛出异常。
## 应用
**栈在下列应用中具有重要的作用。**
1. 直接应用
- 符号匹配。
- 中缀表达式转换为后缀表达式。
- 计算后缀表达式。
- 实现函数调用(包括递归)。
- 求范围误差(极差)(在股票市场中求极差,参看4.8节)。
- 网页浏览器中已访问页面的历史记录(后退(back)按钮)。
- 文本编辑器中的撤销(undo)序列。
- HTML 和XML 文件ф的标签(tag)匹配。
2. 间接应用
- 作为一个算法的辅助数据结构(例如,树遍历算法)。
- 其他数据结构的组件(例如,模拟队列,参见第5章)。
## 实现
**栈抽象数据类型有多种实现方式。下面是常用的方法。**
- 基于简单数组的实现方法。
- 基于动态数组的实现方法。
- 基于链表的实现方法。
### 基于简单数组的实现
- 首先介绍基于简单数组实现栈抽象数据类型的方法。
- 如下图所示,从左至右向数组中添加所有的元素,并定义一个变量用来记录数组当前栈顶(top)元素的下标。

- 当数组存满了栈元素时,执行入栈(插入元素)操作将抛出栈满异常。
- 当对一个没有存储栈元素的数组执行出栈(删除元素)操作时,将抛出栈空异常。

#### **性能和局限性**
**性能:**假设n为栈中元素的个数。在基于简单数组的栈实现中,各种栈操作的算法复杂度如下表所示。

**局限性:**
- 栈的最大空间必须预先声明且不能改变。
- 试图对一个满栈执行入栈操作将产生一个针对简单数组这种特定实现栈方式的异常。
---
### 基于动态数组的实现
- 在上述基于简单数组的栈实现方法中,采用一个下标变量top,它始终指向栈中最新插人元素的位置。当插入(或push)元素时,先增加下标变量top的值,然后在数组中该下标位置存储新元素。类似地,当删除(或pop)元素时,先获取下标变量top位置的元素,然后减小变量top的值。当下标变量top的值等于-1时,表示栈为空。然而仍然需要解决的一个问题是,在固定大小的数组中,如何处理所有空间都已经保存了栈元素这种情况。
- 可以使用数组倍增技术来提高性能。如果数组空间已满,新建一个比原数组空间大一倍的新数组,然后复制元素。采用这种处理方法,执行n次push操作的时间开销为n(不是n^2^)


**性能:**假设n为栈中元素的个数。在基于动态数组的栈实现中,各种栈操作的算法复杂度如下表所示。

**注意:**倍增太多可能导致内存溢出。
### 基于链表的实现
- 使用链表也可以实现栈。通过在链表的表头插入元素的方式实现push操作,删除链表的表头结点(栈顶结点)实现pop操作。


## 栈的各种实现方法比较
#### 递增策略和倍增策略的比较
- 通过分析完成n个push操作的总时间开销T(n)来比较递增策略和倍增策略的区别。
从长度为1的数组表示的空栈开始,一次push操作的平摊时间等于一组push操作的总时间开销的平均值,记为T(n)/n.
- 递增策略:实现push操作的平摊时间开销为0(n)[O(n^2^)/n].
- 倍增策略:实现push操作的平摊时间开销为0(1)[O(n^2^)/n].
### 基于数组实现和基于链表实现的比较
- **基于数组实现的栈**
- 各个操作都是常数时间开销。
- 每隔一段时间倍增操作的开销较大。
- (从空栈开始)n个操作的任意序列的平摊时间开销为O(n)
- 基于链表实现的栈
- 栈规模的增加和减小都很简洁。
- 各个操作都是常数时间开销。
- 每个操作都要使用额外的空间和时间开销来处理指针。
## 栈的相关问题
### 问题1
**如何使用栈来判定括号是否匹配?**
**解答:**对于给定的表达式,可以使用栈来实现括号匹配判定算法。这个算法在编译器中非常重要。解析器每次读入一个字符,如果字符是一个开分隔符(如(、{、或[),那么将其入栈。若读入的是一个闭分隔符(如)、}、或]),那么将栈顶的开分隔符出栈,并与闭分隔符比较。如果两者匹配,则继续解析字符串。如果不匹配,解析器显示匹配错误。下面给出一个时间复杂度为O(n)的基于栈的符号匹配判定算法。
==**算法:**==
- 创建一个栈。
- 当(当前字符不等于输入的结束字符)
- 如果当前字符不是匹配的字符,则忽略它。
- 如果字符是一个开分隔符(如(、{、或[),那么将其入栈。
- 如果字符是一个闭分隔符(如)、}、或]),且栈不为空,栈顶元素出栈,否则提示匹配错误。
- 如果出栈的字符不是相匹配的开分隔符,提示匹配错误。
- 字符串处理结束后,如果栈不为空,则提示匹配错误。

### 问题2
**如何使用栈来实现将中缀表达式转换为后缀表达式的算法?**

- 下面设计表达式转换算法。在中缀表达式中,除非使用括号,否则运算符优先级是隐式的。因此,在设计将中缀表达式转换为后缀表达式的算法时,必须定义运算符的优先级。下表给出了运算符之间的优先级和相关性(计算的先后次序)。

**重要性质**
- 仔细比较中缀表达式2+3*4和后缀表达式234*+,可以发现两种表达式中数字(操作数)的次序是相同的,都是234,但是运算符*和+的次序在两个表达式中是不同的。
- 只需一个栈就可以把中缀表达式转换为后缀表达式。利用栈把表达式中运算符的次序从中序改变为后序。栈中仅存储运算符和左括号"(。由于后缀表达式中不包含括号,所以输出后缀表达式时将不输出括号。
==**算法**==
- 创建一个栈
- ```java
for(输入字符串中的每一个字符 t) {
if(t 是一个操作数) 输出t;
else if(t 是一个右括号)
输出栈并输出该符号,直到一个左括号出栈,(但是左括号不输出-注意右括号也没输出);
else{//t是一个运算符或者左括号
出栈并输出该符号,直到出现一个比t优先级小的符号,或者出现一个左括号,或者空栈;
t入栈;
}
}
![]()