赞
踩
浏览器的前进、后退功能,其实就是利用了栈的数据结构
当我们依次访问一串页面a-b-c,点击浏览器的后退按钮,就可以查看之前浏览过的页面b和a。当你后退到页面a,点击前进按钮,就可以重新查看页面b和c。但是如果后退到b后,点击了新的页面d,那就无法通过前进、后退功能看到页面c了。
先进者后出,后进者先出,这就是典型的“栈”结构。
从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们应该首选“栈”这种数据结构。
从栈的定义中,我们可以发现栈的主要两个操作就是:入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。那么该如何实现一个栈呢?
实际上,栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈叫作:顺序栈,用链表实现的栈,叫作链式栈。
不管是顺序栈还是链式栈,存储数据只需要一个大小为n的数组就够了。在入栈和出栈的时候,只需要一两个临时变量的存储空间,所以空间复杂度为O(1)。空间复杂度是指除了原始的数据存储空间以外,算法运行还需要额外的存储空间。
不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度是O(1)。
要实现一个支持动态扩容的顺序栈,就需要底层依赖一个动态扩容的数组就可以了。当栈满了以后,就申请一个更大的数组,将原来的数据搬移到新数组中。
分析一下支持动态扩容的数据顺序栈的入栈、出栈操作的时间复杂度。对于出栈操作来说,不会涉及到内存的重新申请和数据的搬移,所以出栈的时间复杂度是O(1)。但是对于入栈操作来说,情况就不一样了。当栈中有空闲空间时,入栈操作的时间复杂度是O(1),当空间不够时,就需要重新申请内存和数据搬移,所以时间复杂度就变成了O(n)。
栈的一个比较经典的应用场景就是函数调用栈。操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完毕之后,将这个函数对应的栈帧出栈。(不是很明白这一块的逻辑)
栈的另外一个应用场景就是编译器利用栈来实现表达式求值。编译器通过两个栈来实现的。其中一个保存操作数的栈,另外一个是保存运算符的栈。从左向右遍历表达式,当遇到数字,就直接压入操作数栈,当遇到运算符,就与运算符栈顶元素进行比较。如果比栈顶运算符元素优先级高,就将运算符压入栈;如果比栈顶运算符元素优先级相同或是低,从运算符栈中取出栈顶运算符,从操作数栈的栈顶取2个操作数,然后进行计算,再把计算结果压入栈,继续比较。
下面以3+5*8-6这个表达式的计算过程为例进行图形的展示。
可以借助栈来检查表达式中的括号是否匹配。简化背景,只考虑三种括号:(),[],{},并且可以任意嵌套组合。现在给定一个包含三个括号的表达式字符串,如何检查它是否合法?
用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或是栈中没有数据,则说明为非法格式。
当所有的括号都扫描完成以后,如果栈为空,则说明字符串为合格字符串;否则,则认为非法格式。
用两个栈就可以完美解决这个问题。
比如我们顺序查看了a,b,c三个页面,我们就依次把a,b,c压入栈,这个时候,两个栈的样子如下图所示:
当我们从浏览器的后退按钮,从页面c后退到页面a之后,我们就依次把c和b从栈X放入到栈Y中,这个时候两个栈的数据就是如下图所示:
这时你又想看页面b,点击前进按钮回到页面b,就是把b从栈Y中出栈,放入到栈X当中。此时,栈中的数据如下图所示:
这时如果通过页面b跳转到一个新的页面d了,页面c就无法通过前后退按钮重复查看了,所以需要清空栈Y。此时两个栈如小图所示:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。