栈与队列
栈是限定仅在表尾进行插入和删除操作的线性表. 队列是只允许在一端进行插入操作, 而在另一端进行删除操作的线性表
栈的定义
栈(stack)
是限定仅在表尾进行插入和删除操作的线性表
通常把插入和删除
的一端称作为栈顶(top)
. 另一端就是栈底(bottom)
, 不含有任何数据元素的栈称作为空栈
. 栈又称作为后进先出(Last In First Out)
的线性表. 简称LIFO
结构.
栈的插入操作, 叫作进栈
, 或者也可以叫作压栈
,入栈
.
栈的删除操作, 叫作出栈
, 也可以叫作弹栈
软件被应用的场景: 例如文本Word
, Photoshop
等软件的撤销操作. 最后操作的先进行恢复.
通常栈
的插入和删除操作. 用push
和pop
进行表示. 可以理解为压入
和弹出
栈的顺序存储结构
由于是线性表
的一种特例. 那么同样可以使用数组
来实现顺序存储结构
.
通常数组的0
下标作为栈底
. 代码实现时需要有一个标记来记录栈顶的位置
. 如果栈为一个空栈
那么标记top
通常等于-1
. 当push
入栈一个元素时, 判断top
是否是栈的可容纳的最大值, 如果可以插入, 那么对top
自增, 并对top
的下标位值进行数据的写入. 当pop
出栈反之.
栈的链式存储结构
同样由于栈
是线性表的一种特例. 那么对于链式
可以使用线性表的链式
方式来是实现.
因为栈
的插入和删除操作, 只能在栈顶
进行. 那么就可以把栈顶放在单链表的头部
.
对于链栈
基本上不存在栈满
的情况, 除非内存已经没有可以使用的空间, 如果真的这样, 那么计算机此时会面临着死机崩溃
的情况,而不是链栈
是否溢出的问题.
对于空栈
来说, 只要链表头指针指向空即可.
进栈操作
由于是链表结构
那么就不会如顺序存储结构
那样通过一个标记位
来对数组进行对应的下标
的添加. 因为栈顶为单链表的头部
. 那么如果出现了新的入栈push操作
那么:
- 把原栈顶的元素对象挂载到新入栈的元素对象的
next
上. 完成链的串联. - 把原栈的
top
指向修改为新入栈的元素对象上.
出栈操作
同样相反的操作.
- 修改指向栈顶元素的指针, 栈底方向移动一位.
- 把栈顶元素的
next
指向原第二栈顶位的元素置为null
.
栈的应用-四则表达式
栈的应用最明显的就是递归
. 还有一个有趣的就是四则运算表示式求值
如果要写出一个可以运算包括加减乘除括号的小功能怎么实现?
例如9 + (3 - 1) * 3 + 10 / 2
.
由于优先级乘除
优于加减
. 括号
内部要先进行计算. 这使得普通的方法实现比较麻烦. 如果利用栈
的特性. 其实会很简单.
首先你要了解一个概念后缀表达式
. 对于9 + (3 - 1) * 3 + 10 / 2
这种我们生活中常用的是中缀表达式
这是对于我们人类方便计算的. 而后缀表达式
是针对计算机计算而来的.
后缀表达式
: 是一种不需要括号表达法, 称为逆波兰(Reverse Polish Notation, RPN)
表示. 由于所有的符号都是在要运算数字的后面出现.称为后缀
针对问题9 + (3 - 1) * 3 + 10 / 2
来进行计算. 后缀表达式为9 3 1 - 3 * + 10 2 / +
计算机计算后缀表达式的
规则: 从左到右遍历表达式的每个数字和符号, 遇到是数字就进栈
, 遇到是符号, 就将处于栈顶两个数字出栈
, 进行运算 运算结果进栈, 一直到最终获得结果.
如下图:
整个过程充分利用了栈
后进先出的特性来处理. 用文字描述一下过程:
- 遇到数字直接进栈, 所以栈中的情况为139. 形成了
步骤1
- 遇到
-
号. 栈顶的两个元素1, 3
依次出栈进行3 - 1 = 2
的运算, 并把运算结果2
压入栈, 然后继续后缀表达式
是数字3
压入栈. 形成了步骤2
- 遇到
*
号. 栈顶3,2
出栈计算, 结果6
压回栈内. 此时形成步骤3
- 遇到
+
号. 栈顶6,9
出栈计算, 结果15
压回栈内. 接下来表达式是两个数字10, 2
直接压入栈内. 此时形成步骤4
- 遇到
/
号, 栈顶2,10
出栈计算, 结果5
压回栈内,此时为步骤5
- 遇到
+
号, 栈顶5,15
出栈计算, 结果20
压回栈内, 此时步骤6
. 由于表达式计算完毕. 栈内的唯一一个元素就是计算结果.
后缀表达式的转换
既然知道了计算机可以直接运行后缀表达式
那么, 就来了解日常的标准四则运算表达式或者是中缀表达式
是如何进行转换为后缀表达式
规则
: 从左到右遍历中缀表达式
的每个数字和符号, 如是数字就输出, 即成为后缀表达式
的一部分; 若是符号, 则判断其与栈顶符号的优先级, 是右括号或优先级不高于栈顶符号(乘除优先为加减)则栈顶元素依次出栈并输出, 并将当前符号进栈, 一直到最终输出后缀表达式
位置.
还是上一个表达式的例子
9 + (3 - 1) * 3 + 10 / 2
中缀9 3 1 - 3 * + 10 2 / +
后缀
如图:
图片转换比较难懂的是图3->图4
和图5->图6
图3 -> 图4
由于遇到了中缀表达式
中的右括号)
所以此时需要匹配此前的左括号(
, 所以栈顶依次出栈并输出直到左括号(
出栈为止. 由于左括号
上方只存在-
所以输出-
号.图5 -> 图6
遇到了+
号, 此时栈顶为*
. 根据规则, 比栈顶元素优先级低(没有比+更低的优先级), 所以栈内元素全部出栈, 这时输出为9 3 1 - 3 * +
. 此时中缀
的+
号才被压入了栈内.
队列的定义
队列(queue)
是只允许在一端进行插入操作, 而在另一端进行删除操作的线性表
队列是一种先进先出(First In First Out)
的线性表, 简称FIFO
. 允许插入一端称为队尾
, 允许删除一端称为队头
队列的实际场景: 多人连接客服, 先拨打的最队前头排列, 会被先处理; 键盘进行各种输入,并显示到记事本软件等.
队列的顺序存储结构
队列
作为一种特殊的线性表, 也同样存在顺序
和链式
的结构.
由于队列顺序存储结构
在队头(数组下标0)
出队的时候, 会造成数组角标0的空缺, 那么就需要对所有的队列进行向前的位移. 存在着很大的无用的性能消耗. 所以出现了循环队列
的概念.
循环队列
所谓循环队列
就是在队列的顺序存储结构
基础上. 通过两个指针
分别对应队头
和队尾
. 当出现队头
出栈的时候并需要在进行整体元素的前进操作, 只需要修改指针移向下一个元素. 队尾
也是如此.
但是这里会有一个问题需要处理: 那就是当队头指针front
, 队尾指针rear
. 当队空
或者队满
时这两个指针都是相等的.
解决:
- 设置一个标志位
flag
, 当front == rear
时, 通过flag
是否为0
来判断队是满还是空 - 也可以, 当队空时就是
front == rear
, 始终让数组的大小比队列的大小多一个空闲单元
. 这样当队满时, 数组中就只有一个空闲单元. 这是我们就认为队列已满. 由于rear
可能出现在front
的前或者后. 那么通常(rear + 1) % QueueSize == front
时就可认为队列已满.
所以队列
要实现顺序存储的时候, 如果不使用顺序队列
, 算法的时间性能是不高的. 但循环队列
又面临着数组可能会溢出的问题, 这点要记住.
队列的链式存储结构
队列的链式存储结构, 其实就是线性表的单链表, 只不过它只能尾进头出, 简称链队列.
结构
一个头结点变量
用于引用链头元素结构. 链头元素结构包括数据
和链的下一个元素的指向
. 以此串成链.
入队操作
入队操作就是在链表尾部
插入节点
- 把
尾节点
挂载原始链尾元素
的next下一个元素指向
上.
出对操作
出队操作就是把头结点变量
的指向改为 原链头元素结构的next的指向
.
- 让
头结点变量
指向修改为原链头
的下一个节点. - 并断开出队
链节点
的next
的指向. 清除链连接
的关系.
比较
对于循环队列
与链队列
的比较, 从两方面进行考虑:
时间
: 他们的操作都是常数时间, 即O(1)
, 不过循环队列
是事先申请好空间, 使用期间不释放, 而对于链队列
, 每次申请和释放节点也会存在一些时间开销, 如果入队出队频繁,则两者还是有细微差别的.空间
:循环队列
必须有一个固定的长度, 所以就有了存储元素个数和空间浪费的问题. 而链队列
不会存在这个问题, 尽管需要一个指针域, 会产生一些空间的开销, 但也可以接受, 所以链队列更加灵活.
简要: 在可以确定队列长度最大值的情况下, 建议使用循环队列
, 如果无法估计队列长度那么使用链队列
.