目录
4.1 栈
4.11 栈的定义
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
把允许插入和删除的一端尾栈顶(top),另一端为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进后出(Last In First Out)的线性表,简称LIFO。
首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说是在线性表的表尾进行插入和删除操作,这里表尾是指栈顶,而不是栈底。
它的特殊之处就在于限制了这个线性表的插入和删除位置,它始终只在栈顶进行。这也就使得:栈底是固定的,最先进栈的只能在栈底。
栈的插入操作,叫作进栈,也称压栈、入栈。类似子弹入弹夹,如图4-2-2所示。
栈的删除操作,叫作出栈,也有的叫作弹栈。如同弹夹中的子弹出夹,如图4-2-3所示。
4.2 栈的抽象数据类型
对于栈来讲,理论上线性表的操作特性它都具备,可由于它的特殊性,所以针对它在操作上会有些变化。特别是插入和删除操作,我们改名为push和pop,英文直译的话是压和弹,更容易理解。你就把它当成是弹夹的子弹压入和弹出就好记忆了,我们一般叫进栈和出栈。
4.3 站的顺序存储结构及实现
4.31 栈的顺序存储结构
既然栈是线性表的特例,那么栈的顺序存储其实也是线性表顺序存储的简化,我们简称为顺序栈。线性表是用数组来实现的,想想看,对于栈这种只能一头插入删除的线性表来说,用数组哪一端来作为栈顶和栈底比较好?
对,没错,下标为0的一端作为栈底比较好,因为首元素都存在栈底,变化最小,所以让它作栈底。
若存储栈的长度为StackSize,则栈顶位置top必须小于StackSize。当栈存在一个元素时,top等于0,因此通常把空栈的判定条件定为top等于-1。
来看栈的结构定义:
假设现有一个StackSize为5的栈,则普通情况、空栈和栈满的情况示意图如下:
4.32 栈的顺序存储结构——进栈操作
对于栈的插入,即进栈操作,示意图如下:
对于进栈操作push,其代码如下:
4.43 栈的顺序结构——出栈操作
出站操作pop,代码如下:
两者没有涉及到任何循环语句,因此时间复杂度均为O(1)。
4.4 两栈共享空间
如果有两个相同类型的栈,为它们各自开辟数组的,极有可能是第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多存储空间空闲,造成了一定的浪费。我们可以有一个数组来存储两个栈,充分利用这个数组占用的内存空间。
实现的方法如下图,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,另一个栈的栈底为数组的末端,即下标为数组长度n-1处。这样一来,栈如果增加元素,就是两端点向中间延伸。
关键思路:从数组两端向中间靠拢,只要top1和top2不相遇,两个栈就可以一直使用。
栈空的情况:栈1为空时,top1等于-1,栈2为空时,top2等于n;
栈满的情况:想想极端的情况,若栈2为空时,栈1的top1等于n-1时,栈1满。反之当栈1为空时,栈2的top2等于0时,栈2满,但更多的情况是,两个栈见面之时,也就是两个指针之间相差1时,即top + 1 == top2为栈满。
两栈共享空间的结构代码如下:
对于两栈共享空间的push方法,除了要插入元素值外,还需要一个判断是栈1还是栈2的栈号参数stackNumber,代码如下:
对于两栈共享空间的pop
,也需要判断参数stackNumber,代码如下:
事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。
4.5 栈的链式存储结构及实现
4.51 栈的链式存储结构(链栈)
栈的链式存储结构,简称链栈由于单链表有头指针,而栈顶指针也是必须的,所以可以结合一下,把栈顶放在单链表的头部,这样单链表的头结点也不需要了。对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真发生,则计算机系统已经面临死机崩溃的情况,而不是链栈是否溢出的情问题。
但对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实是top=NULL
的时候。
链栈的结构代码如下:
4.52 栈的链式存储结构——进栈操作
对于链栈的进栈push操作,假设元素值为e的新结点是s,top为栈顶指针,示意图如下:
4.53 栈的链式存储结构——出栈操作
假设变量p用来存储要删除的栈顶结点,将栈顶指针向下移一位,最后释放p即可。如下图所示:
链栈的push和pop没有循环操作,时间复杂度为O ( 1 ) O(1)O(1)。对比顺序栈,两者时间复杂度都为O ( 1 )。二者的区别和线性表一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好使用链栈,反之如果变化在可控范围内,建议使用顺序栈。
4.6 栈的应用——四则运算表达式求值
4.61 后缀(逆波兰)表示法定义
栈的一个常见的现实应用:数学表达式的求值。“先乘除,后加减,从左算到右,先括号内后括号外”,计算机如何实现?这里的困难在于乘除在加减后,却要先运算,而加入了括号就变得更加复杂。但是括号是成对出现的,所以可以利用栈,碰到左括号时,就将此左括号压入栈,不管表达式有多少重括号,遇到左括号就进栈,而后面出现右括号时就让栈顶的左括号出栈,期间数字运算,栈就由空到有元素,最终再因全部匹配成功成为空栈。
但是对于四则运算,括号只是其中一部分,先乘除后加减仍然复杂。1929年,波兰逻辑学家Jan Lukasiewicz发明了一种不需要括号的后缀表示法,称为逆波兰(Reverse Polish Notation,简称RPN)表示。
先来看对于“9 + ( 3 − 1 ) × 3 + 10 ÷ 2 9+(3-1)×3+10÷29+(3−1)×3+10÷2”,用后缀表示成:
正常的表达式:9+(3-1)×3+10÷2
后缀表达式:9 3 1-3 * + 10 2 / +
“9 3 1-3 * + 10 2 / +”这样的表达式称为后缀表达式,因为符号都是在要运算数字的后面出现。
4.62 后缀表达式计算结果
计算机计算后缀表达式:9 3 1-3 * + 10 2 / +
规则:从左到右遍历表达式的每个数字和符号。遇到数字就进栈,遇到符号就将处于栈顶的两个数字出栈进行运算,运算结果进栈,一直到最终获得结果。
4.63 中缀表达式转后缀表达式
接下来推导如何从“9+(3-1)×3+10÷2”变为后缀表达式“9 3 1-3 * + 10 2 / +”。
把标准四则运算表达式
,即“9+(3-1)×3+10÷2”称为中缀表达式
。
规则:从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即称为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级不高于栈顶元素(乘除优先于加减)则栈顶元素依次出栈并输出,并将当前符合进栈,一直到最终输出后缀表达式为止。
4.7 队列的定义
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
4.8 队列的抽象数据类型
4.9 循环队列
线性表有顺序储存结构和链式存储结构,队列作为一种特殊的线性表,同样存在这两种存储方式。先来看看顺序存储。
4.91 队列的顺序存储
队列的顺序存储结构实现和线性表完全相同。
假设一个队列有n个元素,则顺序存储需要建立一个大于n的数组,并把队列的所有元素储存在数组的前n个单元,数组下标为0的一端是队头。所谓的入队列操作就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度是O ( 1 ) 。
而出列在队头,意味着队列的所有元素都得往前移动,时间复杂度为O ( n ) O(n)O(n)。
然后出队列时也可以不全部移动元素,如果不限制队列的元素存储在数组的前n个单元这一条件,出队的性能大大增加。如下图所示。
为了避免只有一个元素时,队头和队尾重合,引入两个指针,front指针指向队头元素,rear指针指向队尾的下一个位置。当front等于rear时,表示空队列
。
4.92 循环队列的定义
解决假溢出的办法就是后面满了,就再从头开始,也就是头尾相接的循环。我们把队列头尾相接的顺序存储结构称为循环队列
。
继续刚才的例图,将rear指针改为指向下标为0的位置,这样就不会造成指针指向不明的情况了,如下图所示。
接着入队a 6 将它置于下标为0处,rear指针指向下标为1处,如左下图所示。若再入队a 7 ,则rear和front指针重合,如右下图所示。
由于rear可能比front大,也可能比front小,所以尽管只相差一个位置时也可能相差整整一圈。若队列的最大尺寸为QueueSize,那么队列满的条件是(rear+1)%QueueSize == front(取模的目的在于整合rear和front大小为一个问题)。比如QueueSize = 5,左上图中front = 0,rear = 4,(4+1)%5=0,所以此时的队列满。再比如下图,front = 2,rear = 1,(1+1)%5=1≠2,所以队列没有满。
当rear > front时,此时的队列长度为rear - front;当rear < front时,队列长度分为两个部分,QueueSize - front和0 + rear,加在一起得rear - front + QueueSize。
因此通用的计算队列长度得公式为:
(rear - front + QueueSize)%QueueSize
4.10 队列的链式存储结构及实现
队列的链式存储结构其实就是线性表的单链表,只不过它只能尾进头出,简称为链队列。
为了操作方便,将队头指针指向链队列的头结点,而队尾指针指向终端结点。
4.10.1 队列的链式存储结构——入队操作
4.10.2 队列的链式存储结构——出队操作
代码如下: