第4章 栈与队列
- 栈是限定仅在表尾进行插入和删除操作的线性表。
- 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。
4.1 开场白
4.2 栈的定义
4.2.1 栈的定义
-
定义:栈(stack)是限定仅在表尾进行插入和删除操作的线性表。把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(LastIn First Out)的线性表,简称LIFO结构。
-
特点:
-
栈元素具有线性关系,即前驱后继关系
-
表尾=栈顶
-
插入=进栈(压栈、入栈),删除=出栈(弹栈)
-
4.2.2 进栈出栈变化形式
- 在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素出栈就可以。
4.3 栈的抽象数据类型
4.4 栈的顺序存储结构及实现
4.4.1 栈的顺序存储结构
-
简称顺序栈,在数组中,一般使用下标为0的一端作为栈底比较好,因为首元素都存在栈底,变化最小,所
以让它作栈底。 -
定义一个top变量来指示栈顶元素在数组中的位置,栈顶位置top必须小于StackSize。当栈存在一个元素时,top等于0,因此通常把空栈的判定条件定为top等于**-1**。
-
结构定义
4.4.2 栈的顺序存储结构——进栈操作
- 代码
- 示意图
4.4.3 栈的顺序存储结构——出栈
- 代码
进栈和出栈的时间复杂度均是O(1)
4.5 两栈共享空间
-
示意图
-
思路:top1和top2在数组的两端,向中间靠拢。两个栈见面之时,也就是两个指针之间相差1时,即top1+1==top2为栈满。
-
代码
-
插入
-
删除
使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,即一个栈增长时另一个栈在缩短的情况
4.6 栈的链式存储结构及实现
4.6.1 栈的链式存储结构
- 定义:简称链栈,一般把栈顶放在单链表的头部(通常对于链栈来说,是不需要头结点的)
-
特点:链栈的空其实就是top=NULL,基本不存在栈满情况。
-
结构
链栈的操作绝大部分都和单链表类似,只是在插入和删除上,特殊一些。
4.6.2 栈的链式存储结构——进栈操作
-
示意图
-
代码
4.6.3 栈的链式存储结构——出栈操作
-
方法:假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可
-
示意图
-
代码
链栈的进栈push和出栈pop操作都很简单,没有任何循环操作,时间复杂度均为O(1)。
如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。
4.7 栈的作用
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心
4.8 栈的应用——递归
4.8.1 斐波那契数列实现
-
兔子繁殖例子:如果兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。假设所有兔都不死,那么一年以后可以繁殖多少对兔子呢?
-
斐波那契递归解决兔子问题
4.8.2 递归定义
- 定义:把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数称为递归函数。
- 特点:每个递归定义必须至少有一个条件,满足时递归不再进行
- 迭代与递归的区别:
- 迭代使用的是循环结构,递归使用的是选择结构
- 递归能使程序的结构更清晰、更简洁、更容易让人理解
- 但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。而迭代则不需要反复调用函数和占用额外的内存
- 递归与栈
- 递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。
- 在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
4.9 栈的应用——四则运算表达式求值
4.9.1 后缀(逆波兰)表示法定义
- 括号:括号都是成对出现的,有左括号就一定会有右括号,对于多重括号,最终也是完全嵌套匹配的。
- 这用栈结构正好合适,只要碰到左括号,就将此左括号进栈,不管表达式有多少重括号,反正遇到左括号就进栈,而后面出现右括号时,就让栈顶的左括号出栈,期间让数字运算,这样,最终有括号的表达式从左到右巡查一遍,栈应该是由空到有元素,最终再因全部匹配成功后成为空栈。
- 先乘除后加减:困难在于乘除在加减的后面,却要先运算
- 解决方法:后缀(逆波兰)表达法:
9+(3-1)×3+10÷2
→9 3 1-3*+10 2/+
4.9.2 后缀表达式计算结果
- 规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
- 可以以
9 3 1-3*+10 2/+
为例进行推导
4.9.3 中缀表达式转后缀表达式
- 中缀表达式定义:标准四则运算表达式,即
9+(3-1)×3+10÷2
称为中缀表达式 - 转换规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表
达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于
栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到
最终输出后缀表达式为止。(具体过程可见书)
要想让计算机具有处理我们通常的标准(中缀)表达式的能力,最重要的就是两步: 1.将中缀表达式转化为后缀表达式(栈用来进出运算的符号,对应于4.9.2)。 2.将后缀表达式进行运算得出结果(栈用来进出运算的数字,对应于4.9.3)
4.10 队列的定义
-
定义:队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
-
特点:队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为
队尾,允许删除的一端称为队头(即队尾插入,队头删除) -
例子:键盘输入,显示器显示
4.11 队列的抽象数据类型
4.12 循环队列
- 队列是线性结构,同样有顺序存储和链式存储
4.12.1 队列顺序存储的不足
- 若数组下标为0的一端是始终是队头,删除操作的时间复杂度为O(1),但插入为O(n)
- 若数组的队头可以移动,入队可能会出现越界错误,但下标较小值可能还是空闲的,会出现假溢出现象
4.12.2 循环队列定义
- 定义:把队列的这种头尾相接的顺序存储结构称为循环队列,以解决假溢出现象
-
问题:空队列时,front等于rear,现在当队列满时,也是front等于rear,那么如何判断此时的队列究竟是空还是满呢?
-
法一:设置一个标志变量flag,当front=rear,且flag=0时为队列空,当front=rear,且flag=1时为队列满
-
法二:当队列空时,条件就是front=rear,当队列满时,我们修改其条件,保留一个元素空间(即队列满时,数组中还有一个空闲单元),判断条件为**(rear+1)%QueueSize==front**(QueueSize为队列最大长度,取模“%”的目的就是为了整合rear与front大小为一个问题)
-
-
队列长度:(rear-front+QueueSize)%QueueSize
-
结构代码
-
初始化代码
-
求队列长度代码
-
入队代码
-
出队代码
4.13 队列的链式存储结构及实现
-
定义:队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们
把它简称为链队列 -
结构代码
4.13.1 队列的链式存储结构——入队操作
-
示意图
-
代码
4.13.2 队列的链式存储结构——出队操作
-
示意图
-
代码
对于循环队列与链队列时间复杂度都为O(1)
在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。