1.栈主要包括两个操作
出栈和入栈;也就是在栈顶插入一个数据和从栈顶删除一个数据;
具有后进先出、先进后出的特性。
 
栈是一种操作受限的线性表,只允许在端插入和删除数据。
 
为什么会有栈这种数据结构?
适合特定场景。从功能上说,数组或者链表都可以替代栈,但是,因为特定的数据结构是对特定场景的抽象,而且数组或者链表暴露了太多的操作接口,操作上确实灵活自由,但是,使用时比较不可控,容易出错。
 
2.数组实现的栈,叫做顺序栈,链表实现的栈,叫做链式栈。
不管是顺序栈还是链式栈,空间复杂度O(1)
 
注:我们所说的空间复杂度,是指除了原本的数据存储空间外,算法运行还需要的额外的存储空间。
不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度都是O(1)
 
3.支持动态扩容的顺序栈
要实现一个支持动态扩容的栈,我们只需要在底层依赖一个支持动态扩容的数组就可以了。
当栈满了以后,我们就申请一个更大的数组,将原来的数据搬迁到新数组中。
如下图:
对于出栈,因为不涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍是O(1).
 
但是对入栈,当栈中有空闲空间时,入栈的时间复杂度为O(1)。但是当空间不够时,就需要重新申请内存和数据搬移了,所以时间复杂度是O(n).
其中入栈操作刚好可以用摊还分析来进行分析。
如下图:当空间不够时,扩容
所以入栈的均摊时间复杂度为O(1).
刚好印证了前面讲的:均摊时间复杂度一般都等于最好情况时间复杂度。
 
4.栈在函数调用中的应用
每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
示例:
如下图
对应的函数栈的调用情况:
 
5.栈在表达式求值中的应用
这里我们将看下编译器如何利用栈来实现表达式求值。
 
实际上,编译器就是通过两个栈来实现的。
其中一个保存操作数的栈,另一个是保存运算符的栈。
我们从左到右遍历表达式,当遇到数字,我们就直接压入操作数栈;
当遇到运算符,就与运算符栈的栈顶元素比较。
如果比运算符栈的栈顶元素的优先级高,就将当前运算符入栈;
如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈中的栈顶取2个操作数,然后进行计算,再把计算结果压入操作数栈,继续比较。
 
例如:3+5*8-6
 
6.栈在括号匹配中的应用
除了用栈来实现表达式求值,我们还可以借助栈来检查表达式中的括号是否匹配。
我们用栈来保存未匹配的左括号,从左到右一次扫描字符串。
当扫描到左括号时,则将其压入栈;当扫描到右括号时,从栈顶取一个左括号。
如果匹配,则继续扫描剩下的字符串。
如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
当所有的括号都扫描完成后,如果栈为空,则说明字符串为合法格式;
否则,说明有未匹配的左括号,为非法格式。
 
7.如何用栈实现浏览器的前进、后退功能。
可以用两个栈来解决这个问题。
 
我们使用两个栈,X和Y,我们把首次浏览的页面依次压入栈X
当点击后退按钮时,再依次从栈中出栈,并将出栈的数据依次放入栈Y。
当我们点击前进按钮时,我们依次从栈Y中取出数据,放入栈X中。
当栈X中没有数据时,那就说明没有页面可以继续后退浏览了。
当栈Y中没有数据,那就说明没有页面可以点击前进按钮浏览了。
 
 
课后思考:
1.我们在栈的应用时,讲到用函数调用栈来保存临时变量,为什么函数调用要用栈来保存临时变量?其他数据结构不行吗?
答:因为函数调用的特点符合先进后出,后进先出的特点。比如在main函数中调用add函数,先开始执行的main函数,需要等add函数执行完毕之后才能执行结束。
正式函数调用的特点,根据数据结构是特定场景的抽象的原则,我们会优先考虑栈结构
 
2.我们都知道,JVM内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储java中的对象。那JVM里面的“栈”跟我们这里说的栈是不是一回事?如果不是?那它为什么又叫做栈呢?
不是一回事,JVM中的堆栈是一种真实存在的物理物质,而数据结构的栈是指满足某种特性的结构。
 
posted @ 2020-05-05 15:36  Vincent-yuan  阅读(598)  评论(0编辑  收藏  举报