大话数据结构—栈与队列

一、栈的定义

栈是(stack)是限定尽在表尾进行插入和删除操作的线性表。
栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。

二、进栈出栈变化形式

注意: 并不是最新进栈的元素只能最后处栈。如,我们现在有三个元素一次进栈,次序会有以下5种:
1. 1、2、2进,再3、2、1出,出栈次序为321;
2. 1进,1出,2进,2出,3进,3出,出栈次序为123;
3. 1进,2进,2出,1出,3进,3出,出栈次序为213;
4. 1进,1出,2进,3进,3出,2出,出栈次序为132;
5. 1进,2进,2出,3进,3出,1出,出栈次序为231。

三、栈的顺序存储结构及实现

(一)栈的顺序存储结构
栈是线性表的特例,栈的顺序存储结构也是线性表存储结构的简化。线性表是用数组实现的,用下标为0的那一端作为栈底使栈变化最小。
我们定义一个top变量来指示栈顶元素在数组中的位置。若存储栈的 长度为StackSize,则栈顶位置top必须小于StackSize。当栈存在一个元素时,top=0.因此,通常把空栈的判定条件定位top=-1.

进栈:push;出栈:pop。(就像子弹的压和弹)。
没有涉及循环,两者的时间复杂度均为1.

两栈共享空间:两个类型相同的栈,则可以共享存储空间。让一个栈的栈底为数组的始端,即下标为0处,另一个栈为数组的末端,即下标为数组长度n-1处。这样,两个栈如果增加元素,就是两端点向中间延伸。两个栈见面之时,也就是两个栈指针相差1,即top1+1==top2为栈满。

(二)栈的链式存储结构
栈的链式存储结构,简称为链栈。
链栈的栈顶和单链表的头指针重合,不需要头结点。
对于链栈来说,不存在栈满的情况,除非内存已经没有可以使用的空间。如果真的发生,就是操作系统已经面临死机崩溃的情况,而不是这个链栈是否真的溢出。
对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top=NULL的时候。

对比顺序栈和链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一定的内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时候很小,有时候很大,那么最好是用链栈,如果它们的变化在可控范围内,建议使用顺序栈会更好一些。

四、栈的作用

有的人可能会问,用数组或链表直接实现功能不就行了吗?为什么还要引入栈这个数据结构呢?
其实这和我们明明有两只脚可以走路,干嘛还要乘汽车、火车、飞机一样。理论上,陆地上的任何地方,你都是可以用双脚走到的,可那需要多长时间和精力呢?我们更关注的是到达而不是如何去的问题。
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的核心问题。反之,像数组等,因为要分散精力去考虑数组下标的增减问题,反而掩盖了问题的本质。
所以,现在的许多高级语言,比如Java,C#等都有对栈结构的封装,你可以不关注它的实现细节,就可以直接使用Stack的push和pop方法,非常方便。

五、栈的应用

(一) 递归
斐波那契数列
(二)四则运算表达式求值
后缀(逆波兰)表示法:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
中缀表示法:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为中缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号有优先级不高于栈顶符号(乘除有限加减)则栈顶元素依次出栈并输出,并将当前符号进栈,直到最终输出后缀表达式为止。

队列

一、队列定义

队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

二、队列的抽象数据类型

同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。

三、循环队列

队列也有线性表的两种存储方式:顺序存储和链式存储。
入队操作是在队尾增加一个元素,时间复杂度为O(1)。但是,出队时,如果规定队列中的元素都放在前n个位置,则第一个元素出队后,后面的元素都要向前移动,以保证对头不为空,也就是下标为0的位置不为空,此时时间复杂度为O(n)。
但是,我们可以想,如果没有队列的元素都必须放在前n个位置的规定,出队的性能就会大大增加。于是,我们因为两个指针,front指向对头元素,rear指针指向队尾的下一个元素,当front==rear,队列为空。这样,当出队时,只需移动front指针即可。但是,这样也会有一个问题,一个队列不可能只有出队,当继续进行入队操作致使队尾已经填满,rear指针指向队列外时,继续入队就可能产生数组越界的错误。但是,由于前面已经进行过出队操作,所以这个队列前面会有空位置,这种现象称为“假溢出”。这里举个例子,现实生活中,当你上了一辆公交车,发现前面有两个空位置,但后排的座位都已经满了。这是,你不会告诉自己,后面没座了,立马下车,等待下一辆。我们都不会这么笨,而都会坐在前面的位置。
这时,就引入了循环队列的概念。循环队列就是首尾相接的顺序存储结构。
那么,问题又来了,前面提到当front==rear时,队列为空,但在循环队列中,这个结论显然不成立。所以,如何判断队列是空还是满呢?以下给出两种方法:
1. 设置一个标志变量flag,当front==rear,且flag=0时队列为空,当front==rear,且flag=1时队列满。
2. 当队列空时,条件就是front=rear,当队列满时,我们修改条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。
我们来重点讨论第二种方法,由于rear可能比front大,也可能比front小,所以它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所以,若队列的最大尺寸是QueueSize,那么队满的条件是(rear+1)%QueueSize==front(取模%的目的就是为了整合front和rear大小为一个问题)
通用的计算队列长度的公式为(rear-front+QueueSize)%QueueSize
循环队列的相关条件和公式:
1. 队空条件:rear==front
2. 队满条件:(rear+1) %QueueSIze==front,其中QueueSize为循环队列的最大长度
3. 计算队列长度:(rear-front+QueueSize)%QueueSize
4. 入队:(rear+1)%QueueSize
5. 出队:(front+1)%QueueSize
到这里,大家可以发现,但是顺序存储,若不是循环队列,算法的时间性能是不高的,但循环队列有面临着数组可能会溢出的问题,所以我们还需要研究一样不需要担心队列长度的链式存储结构。

四、队列的链式存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾尽头出而已,称为链队列。将队头指针指向链队列的头结点,队尾指针指向终端结点。
空队列是,front和rear都指向头结点。
入队操作:就是在链尾插入结点。
出队操作,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点。

对比循环队列和链队列,时间上,其实它们基本操作都是常数时间,即都为O(1)的。不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是存在细微差异。对于空间上来说,循环队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议使用循环队列,如果无法预估队列长度,则用链队列。

你可能感兴趣的:(数据结构)