数据结构与算法之栈篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixin_41923658/article/details/84193754

1、如何理解“栈”?

   栈是一种典型“后进者先出,先进者后出”的结构。从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。

2、如何让实现一个“栈”?

   对于栈,主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据,实际上栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈我们叫做顺序栈,用链表实现的栈,我们叫作链式栈。

  举个例子:

 下面实现一个基于数组的顺序栈。

 

       不管是顺序栈还是链式栈,我们存储数据只需要一个大小为n的数组就可以。在出栈和入栈过程中,只需要一两个临时变量存储空间,所以空间复杂度为O(1)。

   注意,这里存储数据需要一个大小为n的数组,并不是说空间复杂度就是O(n)。因为,这n个空间是必须的,无法省掉。所以说空间复杂度的时候,是指除了原本数据存储空间外,算法运行还需要额外的储存空间。

   对于时间复杂度,不管是顺序栈还是链式栈,入栈、出栈只涉及张定的个别数据的操作,所以时间复杂度都是O(1)。

3、支持动态扩容的顺序栈

      当基于数组实现的栈,是一个固定大小的栈,也就是说,在初始化栈时需要事先指定栈的大小。当栈满之后,就无法再往栈里添加数据了。尽管链式栈的大小不受限,但要存储Next指针,内存消耗相对较多。那如何实现一个可以支持动态扩容的栈呢?

     类比数组,当数组空间不够时,我们就重新申请一块更大的内存,将原来数组中的数据统统拷贝过去。这样就实现了一个支持动态扩容的数组。

    因此,如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。(如下图)

       那么支持动态扩容的顺序栈的时间和空间复杂度又是多少呢?

 

       对于出栈操作来说,因为不会涉及到重新申请和数据的搬移,所以出战的时间复杂度仍然是O(1)。但是,对于入栈操作来说,情况就不一样。当栈中有空闲时间,入栈操作的时间复杂度为O(1)。但当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了O(n)。

       也就是,对于入栈操作来说,最好情况时间复杂度是O(1),最坏情况时间复杂度是O(n).那平均情况下的时间复杂度又是多少呢?还记得我们在复杂度分析那一节中讲的贪还分析法,这个入栈操作的平均情况下的时间复杂度可以用贪还分析法来分析。

      为了方便分析,我们需要事先做一些假设和定义:

      (1)、栈空间不够时,我们重新申请一个原来大小两倍的数组;

      (2)、为了简化分析,假设只有入栈操作没有出栈操作;

      (3)、定义不涉及内存搬移的入栈操作为Simple-push操作,时间复杂度为O(1).

如果当前栈的大小为K,并且已满,当再有新的数据要入栈时,就需要重新申请2倍大小的内存,并且做K个数据的搬移操作,然后在入栈。但是,接下来的K-1次入栈操作,我们都不需要重新申请内存和搬移数据,所以K-1次入栈操作都只需要一个Simple-push操作就可以完成。如下图:

       从中可以看出来,这K次操作,总共涉及了k个数据的搬移以及K次simple-push操作。将K个数据搬移到K次入栈操作,那每个入栈操作就需要一个数据搬移和一个simple-push操作。以此类推,入栈操作的均摊时间复杂度就为O(1)。

      通过以上例子,印证了前面分析,均摊时间复杂度一般等于最好时间复杂度。因为在大部分情况下,如栈操作的时间复杂度为O(1),只有个别时刻才会退化为O(n)。

4、栈在函数调用中的应用

       栈作为一个比较基础的数据结构,应用场景还是蛮多的,比较经典的一个应用场景就是函数调用栈。

      操作系统给每个线程分配了一块独立的内存空间,这块组织成“栈”这种结构,用来存储函数调用的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。举个例子:

 

       从代码中看,main()函数调用了add()函数,获取计算结果,并且与临时变量a相加,最后打印res的值。下图是执行到add()函数时,函数调用栈的情况。

5、栈在表达式求值中的应用

    栈的另一个常见的应用场景,编译器如何利用栈来实现表达式求值。比如:3+5*8-6。实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从从左向右遍历表达式,当遇到数字,我们就直接压入操作栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。如果比运算符栈顶的元素优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取2个操作数,然后进行计算,再把计算玩的结果压入操作数栈,继续比较。

 

  

6、栈在括号匹配中的应用

        举个列子:借助栈来检查表达式中的括号是否匹配?

       假设:表达式中只包含三种括号,圆括号()、方括号[]和花括号{},并且他们可以任意嵌套。比如{[{}]}或[{()}([])等都为合法格式,而{[}()]或[({)]为不合法的格式,那么如何让用栈来解决呢?

       我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个括号。如果能够匹配,比如"("跟")"匹配,"["跟"]"匹配,"{"跟"}"匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能匹配的的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,说明字符串为合法格式;否则,说明有未匹配的左括号,非法格式。

7、开篇问题

如何实现浏览器的前进、后退功能?

   我们使用两个栈,X和Y,我们把首次浏览的页面依次压入栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据依次放入栈Y。当我们点击前进按钮时,我们依次从Y栈中取出数据,放入栈X中。当栈X中没有数据时,那就说明没有页面可以继续后退浏览了。当栈Y中没有数据,那就说明没有页面可以点击前进按钮浏览了。

   比如:你顺序查看了a,b,c三个页面,我们就依次把a,b,c压入栈,这个时候,两个栈的数据就是这个样子:

       当你通过浏览器的后退按钮,从页面c后退到页面a之后,我们就依次把c和b从栈X中弹出,并且依次放入到栈Y。这个时候,两个栈的数据就是这个样子:  

        这个时候你又想看页面b,于是你有点击前进按钮回到b页面,我们就把b再从栈Y中出栈,放入到栈X中。此时两个栈的数据就是这个样子

这个时候,你又通过页面b又跳转到新的页面d了,页面c就无法再通过前进、后退按钮重复查看了,所以需要清空栈Y。此时两个栈的数据就这个样子:

 

欢迎大家扫码关注微信公众号,其中含有有大量免费的人工智能、图像处理、IT资料:

                                                                            Change,There is no better way!

猜你喜欢

转载自blog.csdn.net/weixin_41923658/article/details/84193754