目录
栈的应用——递归 (更详细说明之后参阅《数据结构——从应用到实现》)
栈的定义
栈的初步了解
定义: 栈是限定仅在表位进行插入和删除操作的线性表
我们把允许插入和删除的一端称为栈顶,而另一端称为栈底,包含任何数据元素的栈称为空栈。又称为后进先出的线性表。简称LIFO结构。
具体使用栈的案例:编辑软件上的撤销操作,网页上的返回操作
理解栈的定义:
首先它是一个线性表,也就是说,它具有线性关系,即前驱后继关系;定义中说的是在线性表进行插入和删除的操作,这里表尾是指栈顶,而不是栈底。
它特殊点在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈顶是固定的,最先进栈的只能在栈底。
栈的插入操作,叫做进栈;类似子弹入弹夹;如下图
栈的删除操作,叫作出栈,如同弹夹中的子弹出夹,如下图
进栈出栈变化形式
栈只对线性表插入和删除位置进行了限制,并未对元素进出的时间进行限制,也就是说,再不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要是栈顶元素首先出栈就可以。
举例说明:若我们现在是有3个整形数字元素1、2、3依次进栈,会有哪些出栈次序呢?
从这个简单例子可以看出,3个元素就有5种可能的出栈次序,如果元素数量多,其实出栈的变化将会更多。
栈的抽象数据类型
对于栈来讲,理论上线性表的操作特性它都具备,可由于它的特殊性,所以针对它在操作上会有些变化。
由于栈本身就是一个线性表,那么顺序存储和链式存储,对于栈来说,也是同样适用的。
栈的顺序存储结构
栈是线性表的特例,那么栈的顺序存储也是线性表顺序存储的简化,我们称为顺序栈。
那么思考一下,数组哪一段作为栈底和栈顶比较好?
下标为0的一端作为栈底比较好,因为首元素都在栈底,变化较小,所以让它做栈底。
栈的结构定义
与线性表的顺序存储结构类似,data【maxsize】是栈的最大长度,而top是栈顶位置;栈顶位置必须小于栈的最大长度;当栈存在一个元素时,top=0;因此把空栈的判定条件定为top=-1。
若现在有一个栈,栈的最大长度是5,则栈的普通情况、空栈和栈满的情况如下图
栈的顺序存储结构——进栈操作
top充当着游标的作用,它的值用于充当数组元素的下标,也就是找到当前数组中的数据元素——这也就解释了为什么有一个元素时,top=0,因为那个元素中在data[0]中;而data[0]=e,则说明了这个数据元素的内容是e;
图示如下
栈的顺序存储结构——出栈操作
如果没有要求e返回其值,好像只用top指针-1就满足了出栈要求了
图示如下
两者均未涉及到任何循环语句,因此时间复杂度均是O(1)。
两栈共享空间
其实栈的顺序存储还是比较方便,因为它只准栈顶进出元素,所以不存在线性表插入和删除时需要移动元素的问题。不过它有个很大的缺陷,就是必须事先确定数组存储空间大小。万一不够用了,我们需要编程来扩容,比较麻烦。
但对于两个相同类型的栈,我们却可以最大限度地利用其事先开辟的存储空间来进行操作。
打个比方,就如同两个租客,私密的卧室单独享用,而厨房,厕所等公共区域共同享用,这样房间利用率大大提高,且租房成本下降。
如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,可能会出现以下情况:第一个栈已经满了,再进栈就溢出了;而另一个栈还有很多存储空间空闲。所以我们完全可以用一个数组来存储两个栈。
两栈共享空间——操作原理
做法如图,数组有两个端点,两个栈有两个栈底,让一个栈底作为数组的始端,即下标为0处,另一个栈作为栈的末端,即下标为数组长度n-1处。
如图所示
无论是1号栈为data【0】还是2号栈为data【0】都可以,此时为了直观理解,我们就把左边的1号栈的起始位当做data【0】,2号栈的起始位为data【n-1】。
当两栈共享空间为空的时候
当top1=-1时,1号栈为空栈;当top2=n时,2号栈为空栈;
当两栈共享空间栈满时
由图所示,top1+1=top2时,为栈满
两栈共享空间——操作步骤
两栈共享空间结构如下:
两栈共享空间——进栈
关于共享两栈空间的操作,我们除了需要插入元素值参数外,还需要有一个判断是栈1还是栈2的栈号参数stacknumber。插入元素的代码如下:
因为一开始就判断了是否满栈的情况,所以后面top1+1或者top2-1不担心溢出。
而入栈的代码图解如下
在1号栈插入元素时,top1+1;在2号栈插入元素时,top2-1;
两栈共享空间——出栈
对于两栈共享空间的出栈,就只需要判断1号栈或2号栈是否为空栈,再移动游标进行出栈。
出栈的代码图解如下
栈的链式存储结构
栈只是用栈顶来做插入与删除,此外,单链表有头指针,而栈也要有栈顶指针,所以可以将两者合二为一。栈顶已经在头部了,所以单链表中的头结点也就失去意义。
如图所示
链栈的空栈:top=NULL
链栈的满栈:不存在,除非内存无可使用空间
链栈的结构代码如下:
链栈——进栈操作
s->next=S->next 图解
注意s是结点,S为链表
S->top=s 图解
链栈——出栈操作
图解如下
出栈与进栈时间复杂度均为O(1)
与顺序栈对比
如果栈的使用过程中,元素变化不可预料,有时很小,有时很大,那么用链栈更好;如果变化在可控范围内,建议使用顺序栈更好。
栈的应用——递归 (更详细说明之后参阅《数据结构——从应用到实现》)
在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
栈的递归初步图解https://blog.csdn.net/weixin_41387874/article/details/116800960 不过,现在的高级语言,这样的递归问题不需要用户来管理这个栈的,一切都由系统代劳了。
栈的应用——四则运算表达式
后缀表达式
规则:从左到右遍历表达式的每个数字和符号,遇到数字进栈;遇到符号,将位于栈顶的两个数字出栈,进行运算后的结果进栈。
如表达式 9 3 1-3*+10 2/+ ,图解如下
中缀表达式转后缀表达式
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出;即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
如将表达式“9+(3-1)*3+10/2”转化为9 3 1-3*+10 2/+
在图3与图4中,首次出现括号这一元素。“(”属于符号,因此先进栈,直到“)”出现,将这之间的元素依次从栈顶出栈。而此题中,两括号间的元素为“-”,所以将其输出。
在图5与图6中,图5此时栈顶元素为“+”,即将进栈的符号“*”优先级高于“+”,所以此时不输出,将“*”入栈。
在图6与图7中,即将进栈的符号为“+”,优先级低于此时的栈顶元素“*”,所以栈中元素依次从栈顶输出,输出完后再将“+”入栈。
在图9中,此时表达式已经到最后了,此时即使输出条件不满足,也要依次从栈顶输出了。
从以上推论中可得出,要想让计算机具有处理我们通常的标准表达式的能力,最重要的就是两步:
1. 将中缀表达式转化为后缀表达式(栈用来进出运算的符号)。
2. 将后缀表达式进行运算得出结果(栈用来进出运算的数字)。
队列的定义
使用电脑时,有时会出现死机状态,鼠标怎么点都没用,正打算重启时,它忽然又恢复了,把你刚才点击的所有操作都按顺序执行了一遍——这是因为操作系统中的多个程序因需要通过一个通道输出,而按先后次序排队等待造成的。
使用场景还有移动、电信等客服电话,当客服都被占线时,客户会被要求等待,直到某个客服空下来才能让最先等待的客户接通电话。
这种先进先出的排队功能有一种数据结构可以实现——这就是队列。
定义:队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出的线性表,允许插入的一端叫做队尾,允许删除的一端叫做队头。
如图所示
队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
循环队列
1.队列顺序存储不足
与顺序存储类似,假设一个队列有n个元素,则建立的存储数组需大于n;数组下标为0的一端为队头,另一端便为队尾。所谓的入队操作也就是在队尾追加一个元素,不需要移动任何元素。因此时间复杂度为O(1)。
而队列元素的出列是在队头,即下标为0的位置,那也就意味着,队列中的所有元素都得向前移动,也就是下标为0的位置不为空,此时时间复杂度O(n)。
仔细想想,为什么出队列时一定要全部移动呢,如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加。也就是说队头不一定非要在下标为0的位置。
为了避免只有一个元素时,队头和队尾重合使处理比较麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置。这样当front=rear时,此队列不是还剩一个元素,而是空队列。
下面以假设长度为5的数组为例
起始状态,空队列如左图所示;之后入队a1等元素,front指针依旧指向下标为0位置,rear指针指向下标为4的位置如右图。
出队a1,a2,front指向下标为2的位置,而rear不变,如左图所示;此时再入队a5,却发现rear指针移动到数组之外,如右图所示。
这反映的问题是,若队列最大个数为5,可此时若接着入队的话,因数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,我们的队列在下标为0和1的地方还是空闲的。我们把这种现象称为“假溢出”。
2.循环队列定义
解决“假溢出”问题的一种思路——后面满了,那就重头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
沿用之前的案例,若将rear改为指向下标为0的位置,这样就不会造成指针指向不明的问题了。
若入队a6则如左图所示;再入队a7,则rear指针与front指针重合,如右图所示。
我们说,空队列时,front=rear,可现在满队列时,也是rear=front,如何判断其究竟是空是满?
循环队列判断空满(插入flag变量)
办法一是设置一个flag变量,当front==rear时,且flag=0时为空,反之flag=1时为满。
代码如下所示
初始化队列
flag初始化就定义为0,默认rear=front的情况为空队;当为满队时则进行修改
将元素插入队列
在第一个判断条件中,因为q->flag被初始化为0,所以不会满足该判断条件;若是满足了该判断条件,说明队列为满(一般可能是调错了队列),返回报错。当开始插入元素后,插满时,head指针与tail指针重合,将q->flag=1.
循环队列判断空满(满队留空间)
办法二是当队列为空,条件就为front=rear;队列满时,保留一个空闲单元,如下图所示,此时我们认为队列已经满了;
判断此时队列满的条件是(rear+1)%size==front
队列长度即队列中的元素个数,通用的计算队列的长度公式为:(rear-front+size)%size
循环列表的插入与删除 (用满队留空间的结构来判断空满)
代码如下:
循环队列的顺序存储结构代码:
循环队列的初始化:
循环队列求队列长度:
循环队列入队列操作:
图解如下
循环队列出队列操作:
图解如下
队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
空队列时,front和rear都指向头结点。
链队列的结构为:
队列的链式存储结构——入队操作
入队操作时,其实就是在链表尾部插入结点。
代码如下
图解如下
队列的链式存储结构——出队操作
出队操作时,就是头结点的后继节点出队,将头结点的后继改为它后面的结点。
代码所示
如图所示
循环队列和链队列的比较
循环队列与链队列的比较,从时间上,都为 O(1)的,不过循环队列是事先申请空间,试用期间不释放,而链队列每次申请和释放结点都存在些时间开销;空间上来说,循环队列必须有一个固定的长度,所以有存储元素个数和空间浪费掉问题。而链队列不存在这个问题。