1、第四章 栈与队列,对于栈和队列上的插入、删除操作是受某种特殊限制的。因此,栈和队列也称作操作受限的表,或者限制存取点的表。 本章除了讨论栈和队列的概念、抽象数据类型、表示方法和实现算法外,还将给出一些应用的例子。,4.1 栈及其抽象数据类型 4.1.1 基本概念,栈是一种特殊的线性表,它所有的插入和删除都限制在表的同一端进行。 表中允许进行插入、删除操作的一端叫做栈的顶。 表的另一端则叫做栈的底。 当栈中没有元素时,称之为空栈。 栈的插入运算通常称为进栈或入栈, 栈的删除运算通常称为退栈或出栈。,栈又称为后进先出表(Last In First Out表,简称LIFO表)或下推表,如图所示,4.
2、1.2 抽象数据类型,ADT Stack is operations Stack createEmptyStack ( void )创建一个空栈。 int isEmptyStack ( Stack st )判断栈st是否为空栈。 void push ( Stack st, DataType x )往栈st的栈顶插入一个值为x的元素。 void pop ( Stack st )从栈st的栈顶删除一个元素。 DataType top ( Stack st )求栈顶元素的值。 end ADT Stack,4.2 栈的实现 4.2.1 顺序表示,用顺序的方式表示栈时,栈的类型可定义如下:struct
3、SeqStack /* 顺序栈类型定义 */int MAXNUM; /* 栈中最大元素个数 */int t; /* tMAXNUM,指示栈顶位置,而不是元素个数 */DataType *s;typedef struct SeqStack *PSeqStack; /* 顺序栈的指针类型 */,用顺序存储结构表示的栈的动态示意图,由于栈是一个动态结构,而数组是静态结构,因此会出现所谓的溢出问题。 当栈中已经有 MAXNUM个元素时,如果再作进栈运算,则会产生溢出,通常称为上溢(Overflow); 而对空栈进行出栈运算时也会产生溢出,通常称为下溢(Underflow)。,基本运算的实现,1. 创建
4、一个空栈 PSeqStack createEmptyStack_seq( int m )具体实现与算法2.1类似,需要为栈结构申请空间,不同之处是将栈顶变量赋值为-1。请自己给出。,2. 判断栈是否为空栈int isEmptyStack_seq( PSeqStack pastack ) 当pastack所指的栈为空栈时,则返回1,否则返回0。,3. 进栈运算void push_seq( PSeqStack pastack, DataType x )往pastack所指的栈中插入(或称推入)一个值为的元素。 当栈不满时,先修改栈顶变量,将其值加1,然后把元素x放入栈顶变量所指的位置中。程序实现,
5、4. 出栈运算void pop_seq( PSeqStack pastack ) 从pastack所指的栈中删除(或称弹出)一个元素。 当栈不空时,通过将栈顶变量减1达到元素删除的目的。程序实现,5. 取栈顶元素运算DataType top_seq( PSeqStack pastack ) 当pastack所指的栈不为空栈时,将栈顶元素取出,而栈本身未发生任何变化。 程序实现,4.2.2 链接表示,栈也可以采用链接方式表示,在链接栈中,每个结点的结构可如下定义:struct Node; /* 单链表结点 */typedef struct Node *PNode;/* 指向结点的指针类型 */s
6、truct Node /* 单链表结点定义 */DataType info;PNode link;,为了强调栈顶是栈的一个属性,这里对栈增加了一层封装(后面将会看到:经过这样封装使得栈与队列的链表表示在形式上更加一致),引入LinkStack结构的定义。 struct LinkStack /* 链接栈类型定义 */PNode top; /* 指向栈顶结点 */; typedef struct LinkStack *PLinkStack; /* 链接栈类型的指针类型 */,假设plstack是PLinkStack类型的变量,则plstack-top就是栈顶指针,plstack-top-info是
7、栈顶元素,,运算的实现,1.创建空链接栈 PLinkStack createEmptyStack_link(void) 创建一空链接栈,需要申请链接栈结构(struct LinkStack)空间,将其中top置为NULL,返回该结构的地址。程序实现,2. 判断栈是否为空栈int isEmptyStack_link( PLinkStack plstack )判断plstack所指的栈是否为空栈,当plstack所指的栈为空栈时,则返回1,否则返回0。程序实现,3. 进栈运算void push_link( PLinkStack plstack, DataType x ) 往plstack所指的栈中
8、插入(或称压入)一个值为x的元素。首先申请结点空间,然后通过指针修改,将结点插在栈顶。 程序实现,4. 出栈运算void pop_link( PLinkStack plstack ) 出栈运算,表示从plstack所指的栈中删除(或称弹出)一个元素。当栈不空时,直接修改栈顶指针,删除结点。 程序实现,5. 取栈顶元素 DataType top_link( PLinkStack plstack ) 当plstack所指的栈不空时,取栈顶元素的值,栈保持不变。 程序实现,4.3 栈的应用,4.3.1 栈与递归,本节在引入递归概念的基础上,介绍栈是怎样用来实现递归,以及怎样把一个递归的函数转换成一个
9、等价的非递归的函数。,递归,通常用来说明递归的最简单的例子是阶乘的定义,它可以表示成: 1 若n = 0 n! = n * ( n 1 )! 若n 0 这种用自身的简单情况来定义自己的方式,称为递归定义。在n阶乘的定义中,当n为0时定义为1,它不再用递归来定义,称为递归定义的出口,简称为递归出口。,int fact( int n ) int res=n;if ( n 1 )res=res* fact( n 1 );return res; 函数fact( n )中又调用了函数fact,这种函数自己调用自己的作法称为递归调用。 包含直接还是间接递归调用的函数都称为递归函数。,递归函数的执行过程 假
10、设(主)程序中包含一个k=fact(3)语句,这个语句的执行过程如下页的图所示。在fact(3)的计算过程中,我们实际不需要生成3个相同的fact程序,只要一个程序在不同的阶段能够处理(3份)不同数据。根据后进先出的原则,只要保证把最后调用的程序使用的空间,保存在一个栈的栈顶就可以了。,递归函数到非递归函数的转换,设有一个程序sub要调用函数rout(x),sub本身也可能是一个函数,我们称之为调用函数,而称rout为被调函数。调用函数中使用调用语句rout(a)来引起rout函数的执行,这里a称为实参。x称为形参。,一般来说,函数调用的实现可以分解成下列三步来进行: (1) 传送调用信息。
11、(2) 分配被调函数需要的数据区,并接收传送来的调用信息。 (3) 把控制转移到被调函数的入口。,当被调函数运行结束,需要返回到调用函数时,一般的返回处理也可以分解成下列三步: (1) 传送返回信息。 (2) 释放被调函数的数据区。 (3) 把控制按返回地址转移到调用函数中去。,在非递归调用的情况下,数据区的分配可以在程序运行前进行,一直到整个程序运行结束才释放,这种分配称为静态分配。 在递归调用的情况下,被调函数的局部量不能分配给固定的某些单元,而必须每调用一次就分配一份,当前程序使用的所有的量(包括形参、局部变量和中间工作单元等),都必须是最近一次递归调用时所分配的数据区中的量。即所谓的动
12、态分配。,动态分配通常的处理方法是:在内存中开辟一个存储区域称为运行栈(或简称栈),每次调用时,将动态区指针下推,分配被调函数所需的数据区;在每次返回时,将内存指针上托,释放本次调用所分配的数据区,恢复到上次调用所分配的数据区中;被调函数中变量地址全部采用相对于动态区指针的位移量来表示(称为相对地址)。,根据上述思想,不难给出算法4.9阶乘计算的非递归算法。因为只有一处递归调用,返回地址(locate)和返回值(fact)都容易控制,而且局部量(res)与参数(n)关系十分清楚,所以栈中只要保存一个参数n的值就可以了。这种最简单的递归函数实现过程相当于下面的一个非递归函数。,int nfact
13、( int n ) int res; PSeqStack st; /* 使用顺序存储结构实现的栈 */ st = createEmptyStack_seq( ); while (n0) push_seq(st,n); n = n 1; res = 1; while (! isEmptyStack_seq(st) res = res * top_seq(st); pop_seq(st); free(st); return ( res ); ,4.3.2 迷宫问题,在迷宫中求从入口到出口的所有路径是一个经典的程序设计问题。 迷宫可用图4.5(a)所示的方块来表示,其中每个元素或为通道(以空白方块表
14、示),或为墙(以带阴影的方块表示)。迷宫问题要求的就是:从入口到出口的一个以空白方块构成的(无环)路径。,求解迷宫问题的简单方法是:从入口出发,沿某一方向进行探索,若能走通,则继续向前走;否则沿原路返回,换一方向再进行探索,直到所有可能的通路都探索到为止。这类方法统称回溯法,用回溯法求迷宫问题解的过程,可以用一个栈来保存探索的序列。其算法框架如下: mazeFrame( void ) 创建一个(保存探索过程的)空栈; 把入口位置压入栈中; while 栈不空时取栈顶位置并设置为的当前位置;while 当前位置存在试探可能取下一个试探位置;if (下一个位置是出口) 打印栈中保存的探索过程然后返
15、回if(下一个位置是通道) 把下一个位置进栈并且设置为的当前位置; ,数据结构迷宫可用二维数组mazemn来表示, 数组中元素为0的表示通道,为1的表示墙。 迷宫的入口处为maze11, 出口处为mazem-2n-2, 它们的元素值必为0。 任意时刻在迷宫中的位置可用元素的行下标和列下标(i,j)来表示。,在某一点mazeij时,可能的运动方向有四个。可以建立一个数组direction42 给出相对于位置(i,j)的四个方向上,i与j的增量值。 若在位置(i,j),要进入E方向的位置(g,h),则可根据由该增量值表来修改(i,j)的坐标, g = i + direction00; h = j
16、+ direction01;,栈用顺序存储结构实现,栈中的元素类型DataType说明如下: typedef struct int x,y,d; DataType;,算法的实现 求迷宫中一条路径的算法,可以从入口开始,对每个“当前位置”都从E方向(东方向)试起,若不能通过,则顺时针试S方向、W方向、N方向。 当选定一个可通的方向,即找到“下一个位置”后,要把当前所在的位置纳入到探索路径中,并将当前所在的位置以及所选的方向记录下来,以便往下走不通时可顺原路一步步地退回来,每退一步以后接着试在该点上尚未试过的方向,从“下一个位置”开始继续探索,如此重复直至到达出口。 程序实现: void maze
17、Path(int *maze,int *direction,int x1,int y1,int x2,int y2,int M,int N),4.3.3 表达式计算,为了这里讨论的方便,在不影响问题实质的情况下,我们对表达式做如下简化: (1) 假定所有运算分量都是整数; (2) 所有运算符都是整数的二元操作,且都用一个字符表示。,例如31 (5 - 22) + 70 这类表达式中所有运算符都出现在它的两个运算分量之间,所以称为中缀表达式。 处理系统通常先把表达式转换成另一种等价形式:31 5 22 - 70 +,这种形式称为后缀表达式。 这种表达式里不再需要有括号,每个运算符都出现在它的两个运算分量后面。,