栈与队列(大话数据结构略读)

目录

栈的定义

栈的初步了解

进栈出栈变化形式

栈的抽象数据类型

栈的顺序存储结构

栈的顺序存储结构——进栈操作 

栈的顺序存储结构——出栈操作

两栈共享空间

两栈共享空间——操作原理 

 两栈共享空间——操作步骤

两栈共享空间——进栈

 两栈共享空间——出栈

栈的链式存储结构 

链栈——进栈操作 

链栈——出栈操作 

与顺序栈对比 

栈的应用——递归 (更详细说明之后参阅《数据结构——从应用到实现》)

栈的应用——四则运算表达式

 后缀表达式 

中缀表达式转后缀表达式 

队列的定义

队列的抽象数据类型

 循环队列

        1.队列顺序存储不足

       2.循环队列定义 

循环队列判断空满(插入flag变量)

循环队列判断空满(满队留空间)

        循环列表的插入与删除 (用满队留空间的结构来判断空满)

队列的链式存储结构及实现 

队列的链式存储结构——入队操作 

队列的链式存储结构——出队操作 

循环队列和链队列的比较 


栈的定义

栈的初步了解

定义: 栈是限定仅在表位进行插入和删除操作的线性表

我们把允许插入和删除的一端称为栈顶,而另一端称为栈底,包含任何数据元素的栈称为空栈。又称为后进先出的线性表。简称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)

与顺序栈对比 

        如果栈的使用过程中,元素变化不可预料,有时很小,有时很大,那么用链栈更好;如果变化在可控范围内,建议使用顺序栈更好。

栈的应用——递归 (更详细说明之后参阅《数据结构——从应用到实现》)

         在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。

栈的递归初步图解icon-default.png?t=N7T8https://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)的,不过循环队列是事先申请空间,试用期间不释放,而链队列每次申请和释放结点都存在些时间开销;空间上来说,循环队列必须有一个固定的长度,所以有存储元素个数和空间浪费掉问题。而链队列不存在这个问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值