一、概述
栈跟队列都是保存数据的容器。还有前面的线性表。
栈和队列主要用于计算过程中保存的临时数据,如果数据在编程时就可以确定,那么使用几个变量就可以临时存储,但是如果存储的数据项数不能确定,就需要复杂的存储机制。这样的存储机制称为缓存。栈和队列就是使用最多的缓存结构。
1、栈、队列和数据使用顺序
栈和队列是最简单的缓存结构,它们只支持数据项的存储和访问,不支持数据项之间的任何关系。
因此,它们最重要的两个操作就是存入元素和取出元素。
当然还有其他的操作,比如创建,检查空(或者满)的操作。
按照数据在时间上生成的先后顺序进行不同的处理:
后生成的数据需要先处理。就需要先进后出的数据结构进行存储(栈,Last In First Out)。
先生成的数据需要先处理。就需要先进先出的数据结构进行存储(队列,First In First Out)。
那么如何实现这两种结构呢,最自然跟最简单的实现方式就是使用线性表。
2、应用环境
很多,从略。python中可以直接使用list实现栈的功能。可以使用deque,实现队列的功能。
二、栈:概念和实现
存入栈中的元素之间没有任何具体关系,只有到来的时间先后顺序。
因此,就没有元素的位置和元素的前后顺序等概念。
栈的基本性质保证,在任何时刻可以访问、删除的元素都是在此之前最后存入的那个元素。
2、栈的顺序实现
把list直接当做栈使用,也是可以的,只是这样的对象跟list无法区分,
并且提供了栈不应该支持的list所有操作,这样就会威胁到栈的安全。
3、栈的链接表实现
顺序表实现的优缺点:
优点:表元素放在一个连续的存储块内,方便管理。
缺点:当扩大内存的时候,需要一次复制操作,这是一次高代价的操作。
采用链接表实现的优点,不需要一个连续的存储块就可以,而且没有替换存储的高代价操作。
但是它的缺点零散的存储比较依赖python的存储管理,还有每个结点的链接也会带来开销。
三、栈的应用
栈的用途主要是两方面:
作为程序中的辅助结构,用于保存需要前进后出的数据。
利用栈的先进后出的性质,做一些特定的事情。
1、简单应用:括号匹配问题
问题:
考虑程序中的括号匹配,这里只考虑三种括号,(){}[]。
每种括号都包括一个开括号和一个闭括号,相互对应。
对于括号里面的嵌套,也要正确匹配。
分析:
由于程序中无法预知要处理的括号数量,因此不能使用固定的变量来进行保存,必须使用缓存结构。
需要存储的开括号的使用原则是后存入先使用,符合LIFO原则。
如果一个开括号完成配对,就删除这个括号。
实现过程:
顺序扫描检查正文里的一个个字符。
检查的时候跳过无关的字符
遇到开括号将其压入栈中
遇到闭括号,弹出栈顶元素与之配对。
如果匹配成功继续,直到正文结束。
不过不匹配,以检查失败结束。
2、表达式的表示,计算和变换
表达式和计算的描述
我们常用的表达式是用二元运算符连接起来的中缀表达式。例如:(1+2)*3
中缀表达式是习惯的表达式,但是它有一些缺点,比如不能准确描述计算的顺序,
通常需要借助一些辅助符号(比如圆括号),或者优先级(比如先乘除后加减)。
比如上面的例子中,如果没有(),那么计算顺序将会完全不一样。
因此,当计算机处理表达式的时候,通过会把中缀表达式转化为后缀表达式(逆波兰表达式)。
当然,还有一种表达式是前缀表达式(波兰表达式)。
后缀表达式和前缀表达式都不需要括号或者优先级,或者结合性的规定。
例子:
中缀形式:(3-5) * (6+17*4) / 3
前缀形式:/ * - 3 5 + 6 * 17 4 3
后缀形式:3 5 - 6 17 4 * + * 3 /
后缀表达式的计算
分析后缀表达式的计算规则
遇到运算对象,把它保存起来。
遇到运算符,取出最近保存的两个运算对象,进行运算,将结果保存起来。
遇到问题:
使用什么结构保存数据
表达式里的元素如何表示(可以字符串)
处理失败的情况。
解决方法:
使用栈保存数据。
表达式的元素使用字符串存储
在计算的过程中,如果栈中的元素不足两个,那么操作就失败(说明后缀表达式写的是错误的)。
还有,当计算完成之后,栈里最终只能有一个元素(也就是计算的结果)。
为判断不足两个元素的情况,必须给栈添加一个属性(写成方法也可以),就是栈的深度。
总结:
一、栈:
1、原理:
是一种特殊的线性表,它的底端是封死的,增删操作只能在顶部做。
数据是先入后出。
PUSH:入栈
POP:出栈
2、栈的分类
顺序栈:
核心是数组,在初始化就需要决定开辟栈空间的大小
链式栈:
存储核心是链表,可以任意进行入栈与出栈操作
队列和栈只是一种拿数据和取数据的方式,不管数据结构是怎样的,只要满足先入后出就是栈
只要满足先入先出就是队列。
链表,数组,hash表等,是属于存储结构,有不同的存储空间个结构。
核心在于top(栈顶指针),出栈返回top–,入栈top++
实际上,我们在执行方法的时候,也会有一个方法栈,方法内部调用方法,会先返回内部方法的结果,
这也是一种压栈入栈。
二、队列:
1、原理:
队列是一种在队列尾端进行插入,而在队列头端进行删除的线性表。
队列的插入操作称为入队(push),删除操作称为出队(pop)。
数据是先进先出
2、队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,
因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。
添加元素时,元素只能从队尾一端进入队列
链表队列:
链表头部为队首,链表尾部为队尾。
存储一个指向队尾的指针,方便从链表尾插入元素
使用带头节点的链表,方便从链表头删除元素。
3、Queue:
Queue接口与List、Set同一级别,都是继承了Collection接口。
内置的不阻塞队列:
PriorityQueue :有序队列,可以按照自然排序或者java.util.Comparator 实现来定位。
ConcurrentLinkedQueue :是基于链接节点的、线程安全的队列。并发访问不需要同步。
实现阻塞接口的:
ArrayBlockingQueue :
基于数组实现的一个有界队列,
在构造时需要指定容量, 并可以选择是否需要公平性
(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:
即等待时间最长的线程会先操作)
它是基于数组的阻塞循环队列
LinkedBlockingQueue:
基于链表的队列,容量是几乎没有上限的(Integer.MAX_VALUE)
PriorityBlockingQueue:
带优先级的 队列,而不是先进先出队列。元素按优先级顺序被移除
DelayQueue:
存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。
头部是延迟期满后保存时间最长的 Delayed 元