zoukankan      html  css  js  c++  java
  • 数据结构与算法--栈、队列(栈)

    hello,everybody. 我们又见面了,这次我们一起来学习数据结构中,非常有意思的两种结构—Stack ,Queue.

    首先来学习一下栈:

    栈:限定只在表尾进行删除插入操作的线性表。

    顾名思义,栈是一种特殊的线性表。它特殊在什么地方呢?它只能在表尾进行插入或删除操作,又就意味着,它只能是先进后出。给大家举个现实中,利用栈的例子。我们都用浏览器浏览过网页,我们对浏览器的前进后退按钮一定都不陌生。当我们打开第一个网页时,有一个图片链接,于是我们又跳到了第二个网页。此时,又有一个文字链接,我们又跳到了第三个网页。此时,我点击浏览器的后退按钮,我们会回到第二个网页。再点击浏览器的后退按钮,我们又跑到了第一个网页。是不是最先打开的第一个网页,是最后一个恢复的?是不是,先进后出?

    看来,我们在学习第二章线性表时,付出的心血没有白费。看看,我在学习栈与队列时,感到很轻松,因为它讲的一些概念,我都掌握了。所以,我们在学习知识时,一定要踏实,耐心。付出一定会有回报的,你用心付出,回报巨大,回报明显。你不用心付出,回报微小,回报不明显。你不付出,只是随意看看,那么当别人说起这些知识时,你也可以装下B。所以,付出是有回报的。只是为了,回报巨大,回报明显,我们需要用心,态度要端正。

    我们把允许删除的一端称为栈顶(Top),另一端称为栈底(Bottom).不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表。大家需要注意了,我们说,栈是限定了,只能在表尾进行删除插入操作的线性表。这里所说的表尾,是指栈顶,而不是栈底。

    栈的插入操作,叫作进栈,栈的删除操叫做出栈。如下图:

    image

    栈的抽象数据类型:

    image

    既然栈也是线性表,那么我们上章所学的线性表操作,也同样适用于栈。那么,栈也同样有顺序存储,与链式存储的存储方式。

    栈的顺序存储结构:

    我们在学习线性表的顺序存储时,我们是用数组来表示的。对于栈的顺序存储,我们同样可以用数组来表示。那么大家思考一下,我们用哪头来表示栈顶,哪头来表示栈底呢?

    因为,插入、删除操作都是在栈顶进行。相对于栈顶,栈底是比较”安静的”,比较稳定。那么,我们就用下标为0的位置,来定义我们的栈底。大家看下图:

    image

    我们用Top来指向栈顶元素所在数组的下标,这个Top就相当于游标卡尺的游标。游标可以变大变小,意味着我们的Top可以变大表小,但是再怎么变,它也不能超过游标卡尺的长度。Top,不能超过我们的数组长度。当栈存在一个元素时,我们定义Top指向0.当栈为空时,我们定义Top指向-1.

    栈的结构定义:

    image

    进栈操作:

    Status Push(SqStack *S,SElemType e)

    {

            if(S->top==MAXSIZE-1) /*栈满*/

           return ERROR;

         S->top++;

       S->data[S->top]=e;/*将元素e添加栈顶空间*/

    return ok;

    }

    出栈操作:

    Status Pop(SqStack *S,SElemType *e)

    {

               if(S->top==-1)

               return ERROR;

            *e=S->data[S->top];/*将栈顶元素赋值给e*/

             S->top—;

    return ok;

    }

    两者没有涉及任何的循环操作,所以时间复杂度为O【1】.

    其实,栈的顺序存储还是很方便的。因为它只允许在表尾进行删除插入操作,不必移动其他元素。但是它有一个很大的不足,我们事前必须清楚数组的大小,给少了,溢出。给多了,浪费。

    两栈共享空间:

    对于两个相同类型的栈,我们可以把他们结合起来,用一个数组来存储。充分利用数组的空间,提高效率。数组有两个端点,栈也有两个断点。我们可以用数组下标为0的位置,存放栈的栈底。数组下标为n-1的位置,存放另一个栈的栈底。这样两个栈如果插入元素,就是从数组两端向中间插入数据。如下图:

    image

    这样,我们就充分利用了数组的空间。大家可以发现,Top1是栈1的栈顶指针,Top2是栈2的栈顶指针。只要Top1与Top2不见面,两栈就可以一直使用。那么当栈1为空栈时,Top1=-1.栈2为空栈时,Top2=n。那么什么时候栈满呢?

    想想极端情况,栈2为空栈。当Top1=n-1时,栈1为满栈。栈1为空栈时,Top2=0时,栈2为满栈。但是,更多的时候,是Top1与Top2见面时。也就是它们相差1.Top1+1=Top2.

    下图是两栈共享空间结构

    image

    插入元素e为新栈顶元素:

    Status Push(SqDoubleStack *S,SElemType e, int StackNumber)

    {

         if(S->top1+1==S->top2)/*栈满了*/

    return ERROR;

      if(StackNumber==1)/*栈1有元素进栈*/

    S->data[++S->top1]=e;/*Top1+1,把e赋值给栈顶元素*/

    else if(StackNumber==2);/*栈2有元素进栈*/

    S->data[--S->top];/*Top—,把e赋给栈2的栈顶元素*/

    }

    出栈操作:

    Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)

    {

                   if(stackNumber==1)

                         {

                                  if(S->top1==-1)/*栈1为空栈*/

                                   return ERROR;

                             *e=S->data[S->top1—];

                     }

    else  if(stackNumber==2)

    {

            if(S->Top2==Maxsize)/*栈2为空栈*/

        return ERROR;

      *e=S->data[S->top2++];

    }

    return ok;

    }

    栈的链式存储结构及定义:

    好了,我们来学习一下栈的链式存储结构吧,简称链栈。

    因为链表有头指针,而栈又有栈顶指针,那么为何不让它们合二为一呢?所以,我们通常将头结点定义为栈顶。因为已经有了栈顶在头部了,头结点作用就不大了。所以,对于链栈,是不需要头结点的。

    image

    对于空表来说,原来的链表是头指针指向空,那么链栈的空其实就是Top=NULL的时候。

    链栈的结构代码如下图:
    image

    链栈的插入:

    Status Push(LinkStack *S,SElemType e)

    {

               LinkStackPtr  s=(LinkStackPtr)malloc(sizeof(StackNode));

              s->data=e;

             s->next=S->top;/*将原栈顶元素改为新元素的后继元素*

             S->top=s;/*将新元素S赋给栈顶元素*/

          S->count++;

    return OK;

    }

    /*链栈的删除*/

    Status Pop(LinkStack *S,SElemType *e)

    {

               LinkStackPtr p;

       if(StackEmpty(S))

    return ERROR;

       *e=S->top->data;

          p=S->top;

    S->top=S->top->next;

    free(p);

      S->count—;

    return OK;

    }

    大家可以发现,链栈的入栈与出栈的算法,都没有循环语句,时间复杂度都是O【1】.我们可以发现,顺序存储的栈与链式存储的栈,他们的时间性能都是一样的。不同的是,空间性能上。线性栈,需要事前给出固定长度,这就可能造成内存空间浪费的可能。但是,线性栈,存取时定位很方便。而链式存储虽然不用事前分配固定大小,但是每个元素必须开辟指针域,这也造成了空间的消耗。线性栈与链栈与线性表与链表是一样的,当我们的元素变化比较大时,建议使用链栈,否则使用线性栈。

    栈的作用:

    大家可能会有疑问,既然栈也是一种线性表。那么我们已经有了线性表,为什么还要学习栈呢?李白说过:”天生我材必有用”。存在即合理,既然有这种数据结构,就一定有它的用武之地。线性表相当于我们的脚,理论上我们用脚可以走遍地球上 任何地方。但是,从北京到上海,我们是选择用脚走,还是乘交通工具呢?栈,就相当于飞机、火车、汽车等交通工具。它的出现,是为了简化程序的设计,使我们把精力全部放在问题本身上。是不是不好理解啊,呵呵,没事儿的,我们一起来看看栈的实际用途吧。

    栈的应用:

    栈的一个重要应用,就是实现了程序设计语言的递归操作。什么是递归呢?通过学习一个经典的算法来了解递归的含义吧。

    斐波那契(Fibonacci)数列实现:

    1 1 2 3 5 8 13 21……..

    前面相邻的两项的和,构成了后面的项。像这样的数列,就叫做斐波那契数列。首先,我们设计一个算法实现这个斐波那契数列。

    void Fibonacci()

    {

           int i;

           int a[40];

           a[0]=0;

           a[1]=1;

         printf(“%d ”,a[0]);

          printf(“%d ”,a[1]);

    for(i=2;i<40;i++)

    {

        a [i]=a[i-1]+a[i-2];

    printf(“%d ”,a[i]);

    }

    }

    运行结果:

    image

    我们设计的这个算法,是利用迭代功能实现的,我们将这个利用迭代实现斐波那契算法成为迭代斐波那契。

    现在,我们再来看看利用递归实现斐波那契的算法。

    int Fbi(int i)

    {

          if(i<2)

    return i==0?0:1;

    return Fbi(i-1)+Fbi(i-2);

    }

    运行结果:

    image

    两种算法,结果是一样的。大家可以观察研究一下这两个算法,通过观察,我们发现利用递归斐波那契算法要比迭代斐波那契算法干净,简洁。所谓的递归,不过是一直在调用自己。那么我们就好了递归的含义.

    递归:我们把一直调用自己或通过一系列语句间接地调用自己的函数,称为递归。

    设计递归算法时,最怕的就是陷入无限的调用中。所以,我们会给出一个条件,当递归函数满足这个条件时,结束递归调用。这也是,这两个算法的不同点。迭代算法,递归算法,说白了,前者是使用循环结构,而后者使用选择结构。递归算法,使得代码简洁,易懂,减少了理解代码的时间。但是,会创建多个函数副本,会耗费大量的时间和内存。迭代则不用反复调用函数和占用额外的内存。

    因此,我们要根据实际情况来选择不同的代码实现方式。

    那么,讲这么多,递归跟栈到底有什么关系呢?大家可以发现,递归是一层层的在调用函数,我们把调用函数想象成栈的入栈,我们把参数、调用地址都压入栈中。当满足判断条件时,我们在将位于栈顶的参数、调用地址弹出栈中。这样,函数就会一层一层的返回,直至最先调用的函数。这样大家是不是就明白了,栈是如何实现递归的吧。

    栈还有一些其他应用:

    栈的现实应用还有很多,我们来学习一下用的比较多的一个应用:四则运算表达式。

    我们都用过高级计算器,它的高级之处是,可以进行带括号的四则运算。那么计算器内部是如何实现的呢?就是利用栈这个数据结构实现的,一起来学习一下吧。

    后缀表示法:

    首先,我们要先了解一下后缀表示法。波兰逻辑学家,想到了一种不需要括号的后缀表达法,我们称为后缀表示法。称为后缀表示法,是因为运算符全在要计算数字的后面。例如,9+(3-1)*3+10/2,对于这个四则运算表达式,转换成后缀表达式就是9 3 1 – 3 *+10  2  +   .大家看这个后缀表示法是不是很别扭,你不喜欢,可是计算机非常喜欢的。

    中缀法表达式转后缀法表达式:
    我们把普通的四则运算表达式称为中缀法,那么如何从中缀法转后缀法呢?从左向右依次遍历表达式,遇到数字就输出,遇到符号,如果是右括号或是优先级高于栈顶符号的,就将栈中符号输出,外面符号进栈。我们一起来做个练习吧,将中缀法9+(3-1)*3+10/2,转为后缀法。

    第一步,初始化一个空栈,用来对符号进出栈使用。如下图左:

    image

    第一个字符是9,输出9,后面是符号+,此时栈顶为空,没有低于+的符号,所以+进栈,如上图右.

    第三个字符是(,因为是左括号,说明还未配对,故进栈。如下图左所示:

    image

    第四个字符是3,输出,此时输出的字符为 9 3 .

    第五个字符是-,进栈。如上图右。

    第六个字符是1,输出。此时输出的为 9 3 1

    第七个字符是),要匹配),所以栈内符号依次出栈,直到匹配到).现在输出为 9 3 1 -

    此时栈内符号只有一个+。如下图左:

    image

    第八字符是*,因为此时栈顶元素是+,比*级别低,所以不用出栈,*进栈。如下图又:

    image

    第九个字符是3,输出, 此时输出为 9 3 1 – 3

    第十个字符是+,因为此时栈顶元素是*,比+级别高,所以出栈,(栈里有* +  都不必+的运算级别低,所以全部输出)。此时输出*  +    第十个字符+入栈。此时栈如下图做所示:

    image

    此时输出的为 9 3 1 – 3 * +

    第十一个字符为10,输出10    .9 3 1 – 3 * + 10

    第十二个字符为/,因为栈顶元素为+,比/运算级别低,所以/进栈。如上图右.

    第十三个字符为2,输出2.

    没有字符了,所以栈内符号依次 输出。

    image

    最终输出为9 3 1 – 3 * + 10 2 / +

    以上就是中缀法转后缀法。

    我们有了后缀法的表达式,就可以让计算机来计算了。那么计算机是如何计算的呢?

    后缀表达式计算结果:

    规则,从左到右依次遍历后缀法表达式,遇到数字就进栈,遇到符号,就将栈内头两个数字出栈进行计算,计算结果再入栈,直到结算完为止。我们就拿9+(3-1)*3+10/2的后缀法9 3 1 – 3 * 10 2 / +来练习。

    前三个字符分别为9 3 1,依次入栈。如下图:

    image

    第四个字符为-,栈内头两个数字出栈,计算结构入栈。1、3 出栈。执行3-1 结果 2  入栈。

    第五个字符3 入栈 。如下图:
    image

    第六个字符*,栈内数字3、2出栈。执行2*3,结果6入栈。如下图:

    image

    第7个字符+,6、9出栈,执行9+6,结果15入栈。如上图右。

    第八、九字符分别为10 ,2依次入栈。

    第10字符为/,10  2  出栈,执行10/2 结果5入栈,如下图:

    image

    第十一个字符为+,栈内数字5 15 出栈,执行15+5 结果20.入栈

    因为是最后一个字符,所以20出栈,栈为空。20是最终的计算结果。如下图:

    image

    计算机就是这样执行四则运算的,到这里,你应该能明白栈、线性表同样是线性表,为什么又要学习栈了。

    好了,这就是栈全部内容,休息一下,我们再来学习队列。

  • 相关阅读:
    分享Kali Linux 2016.2第41周镜像虚拟机
    Visual Studio工具栏中无法选择调试设备
    Xamarin.iOS编译出错
    Xamarin基础命名空间Microsoft.SqlServer.Server
    编译包含Google Play服务App的SDK版本问题
    解决Fiddler无法抓到手机的会话包
    Xamarin.Android编译CPU类型选择方式
    Delphi iOS
    Delphi Android
    CRC16-循环冗余校验
  • 原文地址:https://www.cnblogs.com/VitoCorleone/p/3780377.html
Copyright © 2011-2022 走看看