770 likes | 929 Views
第三章 栈和队列. 本章主要内容: • 栈和队列的抽象数据类型定义 • 栈和队列的表示和实现 • 栈和队列的应用 学习目的及要求: • 掌握并理解栈和队列是两种特殊的线性结构 • 掌握栈和队列的抽象数据类型的定义以及表示和实现 掌握栈和队列的典型应用. 第三章 栈和队列. 3.1 栈 3.2 栈与递归的实现 3.3 队列. 3.1 栈. 一、栈的定义 二、抽象数据类型栈的定义 三、栈的表示和实现 四、栈的应用举例. 一、 栈的定义. 1 .定义
E N D
第三章 栈和队列 本章主要内容: •栈和队列的抽象数据类型定义 •栈和队列的表示和实现 •栈和队列的应用 学习目的及要求: •掌握并理解栈和队列是两种特殊的线性结构 •掌握栈和队列的抽象数据类型的定义以及表示和实现 • 掌握栈和队列的典型应用
第三章 栈和队列 3.1 栈 3.2 栈与递归的实现 3.3 队列
3.1 栈 一、栈的定义 二、抽象数据类型栈的定义 三、栈的表示和实现 四、栈的应用举例
一、 栈的定义 1.定义 栈(Stack)——是一种特殊的线性表,其插入和删除操作均在表的一端进行,是一种运算受限的线性表。 栈顶(top)——允许插入和删除的一端,又称表尾。 栈底(bottom)——栈的另一端,又称表头。 若给栈S=(a1,a2…ai,…an),则a1 为栈底元素,an 为栈顶元素,如图所示。 进栈(push)——在栈顶插入一个元素, 又称入栈或压入。 出栈(pop)——在栈顶删除一个元素, 又称退栈或弹出。 空栈——栈中没有元素(n=0)。
一、 栈的定义 2.栈的特点 后进先出(Last In First Out,简称LIFO)。 又称栈为后进先出表(简称LIFO结构)。
二、 抽象数据类型栈的定义 对栈的操作除了在栈顶进行插入和删除外,还有栈的初始化、判空及取栈顶元素等。 其抽象数据类型定义如下: ADT Stack { 数据对象:D={ai|ai∈ElemSet,i=1,2…n,n≥0} 数据关系:R={<ai-1,ai> |ai-1,ai∈D,i=2,3,…,n} 基本操作: InitStack(&s) 操作结果:构造一个空栈s。 DestroyStack(&s) 操作结果:栈s被销毁。 ClearStack(&s) 操作结果:将s清为空栈。
二、 抽象数据类型栈的定义 StackEmpty(s) 操作结果:若s为空栈,则返回TRUE,否则FALSE。 StackLength(s) 操作结果:返回s的元素个数,即栈的长度。 GetTop(s,&e) 操作结果:用e返回s的栈顶元素。 Push(&s,e) 操作结果:插入元素e为新的栈顶元素。 Pop(&s,&e) 操作结果:删除s的栈顶元素,并用e返回其值。 StackTrasverse(s,visit()) 操作结果:从栈底到栈顶依次对s的每个数据元素调用 函数visit()。一旦visit()失败,则操作失效。 }ADT Stack
三、 栈的表示和实现 栈也有两种存储表示方式:顺序存储和链式存储。 1.栈的顺序存储及其基本操作的实现 (1)栈的顺序存储是利用一块连续的存储单元依次存放栈中的数据元素,并附一个指针top指示当前栈顶。 采用顺序存储方式存储的栈称为顺序栈。 (2) C语言描述顺序栈: ①用一维数组和一个栈顶指针 Typedef struct { elemtype stack[MaxSize]; int top; }stacktype; 4 3 2 1 0 -1空栈 top c b top a top
三、 栈的表示和实现 ②用一个栈顶指针和一个栈底指针以及一个初始的存储空间 #define STACK_INIT_SIZE 100 #define STACKINCREMENT 10 typedef struct { selemtype *base; selemtype *top; int stacksize; }sqstack; 连续的存储空间 栈顶指针指示栈顶元素当前位置(用栈顶指针动态地反映栈中元素的变化情况) top top b top 空栈 a base base
三、 栈的表示和实现 (3)基本操作的算法 ① 用一维数组和一个栈顶指针 A.定义一个栈 设栈中数据类型为整型,用stack数组存放栈中数据元素,定义一个栈类型stacktype: typedef int elemtype; #define MaxSize 100; Typedef struct { elemtype stack[MaxSize]; int top; }stacktype;
三、 栈的表示和实现 B.初始化栈:initstack(s) 建立一个空栈s,实际上是将栈顶指针指向-1。 Void initstack(stacktype *s) { s->top=-1;} C.判断栈是否为空栈:stackempty(s) 若栈s为空则返回1,否则返回0。 Int stackempty(stacktype *s) { if(s->top==-1) return (1); else return(0);} 空栈 top
三、 栈的表示和实现 D.进栈:push(s,x) 将元素x 插入到栈s中。若栈满则显示相应信息,否则指针top增1,将x送入栈顶指针所在位置。 Void push(stacktype *s, elemtype x) {if(s->top==MaxSize-1) printf(“stack overflow\n”); else { s->top++;s->stack[s->top]=x;} }
三、 栈的表示和实现 E.出栈:pop(s) 从栈中删除栈顶元素。若栈空则显示相应信息,否则栈指针减1,即栈顶为下一个元素的位置。 elemtype pop(stacktype *s) {elemtype e; if(s->top==-1) printf(“stack underflow\n”); else {e=s->stack[s->top]; s->top--; } return e;}
三、 栈的表示和实现 进栈 出栈 e top c top c c b top b top b top a top a top
三、 栈的表示和实现 F.取栈顶元素:gettop(s) 若栈为空则显示相应信息,否则返回栈顶元素,保持栈顶指针不变。 注意:与弹出操作的区别 Elemtype gettop(stacktype *s) { if(s->top==-1) printf(“stack empty\n”); else return(s->stack[s->top]); } G.显示栈中元素:display(s) void display(stacktype *s) { int i; printf(“stack data:”) for(i=s->top;i>=0,i--) printf(“%d”,s->stack[i]); printf(“\n”); }
三、 栈的表示和实现 ②用一个栈顶指针和一个栈底指针以及一个初始的存储空间 A.定义一个栈 #define STACK_INIT_SIZE 100 #define STACKINCREMENT 10 typedef struct { selemtype *base; //栈底指针 selemtype *top; //栈顶指针 int stacksize; //当前已分配存储空间,元素个数 }sqstack;
三、 栈的表示和实现 B.初始化栈:initstack(s) 构造一个空栈s,即s.top=s.base void initstack(sqstack *s) {s->base=(selemtype*(malloc(stack_inti_size*sizeof(selemtype)); if(!s->base) printf(“fail!\n”); else {s->top=s->base; s->stacksize=stack_init_size; } }
三、 栈的表示和实现 C.判断栈是否为空栈:stackempty(s) 若栈s为空则返回1,否则返回0。 Int stackempty(sqstack *s) {if (s->top==s->base) return(1); else return(0);} D. 取栈顶元素:gettop(s) 若栈为空则显示相应信息,否则返回栈顶元素,保持栈顶指针不变。 selemtype gettop(sqstack *s) { if(s->top==s->base) printf(“stack empty\n”); else return(*(s->top-1)); }
三、 栈的表示和实现 E. 进栈:push(s,x) 将元素x 插入到栈s中。若栈满则追加存储空间,否则将x送入栈顶,指针top增1。 Void push(sqstack *s, elemtype x) {if(s->top-s->base>=s->stacksize) {s->base=(selemtype*)realloc(sbase,(s.stacksize+ STACKINCREMENT)*sizeof(selemtype)); s->top=s->base+s->stacksize; s->stacksize+=STACKINCREMENT; } *s->top++=x; } //栈满的判断 //重新定义栈空间 //新栈顶 //进栈
三、 栈的表示和实现 F. 出栈:pop(s) 从栈中删除栈顶元素。若栈空则显示相应信息,否则栈指针减1,即栈顶为下一个元素的位置。 selemtype pop(stacktype *s) {selemtype e; if(s->top==s->base) printf(“stack underflow\n”); else {s->top--; e=*s->top; } return e;}
三、 栈的表示和实现 12.栈的链式存储方式 采用链式存储的栈称为链栈。 链栈的特点: (1)不存在栈满上溢的情况,是一种特殊的单链表。 (2)链栈是动态存储结构,无需预先分配存储空间,因此节省存储空间。 (3)入栈时,先申请一个结点的存储空间,然后修改栈顶指针。 (4)出栈时,同样也是将栈顶的后继结点做为栈顶。
三、 栈的表示和实现 (1)链栈结点类型的定义: typedef struct node {elemtype data; struct node *next; }node; (2)初始化栈 void initstack(node *top) {top=NULL;} data next top top an 栈顶 NULL 空栈 an-1 … a1 ^ 栈底 相当于链表的head
三、 栈的表示和实现 (3)入栈 void push(node *top,elemtype e) { node *p; p=(node*)malloc(sizeof(node)); p->data=e; p->next=top; top=p; } (4)出栈 elemtype pop(node *top) {node *q; elemtype *p if(top!=NULL) {q=top; *p=top->data; top=top->next; free(q); return(*p);} }
四、 栈的应用举例 四、栈的应用举例 1. 数制转换 由十进制N向其它进制d的转换是计算机实现计算的基本问题,基于原理: N=(N div d)*d+N mod d 其中:div为整除运算,mod 为取余运算。 一个十进制数转换成其它进制数: N除d取余,先余为低,后余为高。 如:(1348)10=(2504)8 1348 mod 8 4 0 5 2 1348 8 低 高 168 8 21 8 8 2 0
四、 栈的应用举例 算法分析: 由十进制转换为其他进制的数的规则,可知,求得的余数的顺序为由低位到高位,而输出则是由高位到低位。因此,这正好利用栈的特点,先将求得的余数依次入栈,输出时,再将栈中的数据出栈。
四、 栈的应用举例 算法实现: void conversion( ) {initstack(s); scanf(“%d”,n); while(n) {push(s,n%8); n=n/8; } while(!stackempty( )) { pop(s,e); printf(“%d”,e); } } //余数入栈
四、 栈的应用举例 2.表达式求值 表达式求值是程序设计语言编译的一个最基本的问题。它的实现是栈应用的又一典型例子。 仅以算术表达式为例。 (1)算符优先法 • 根据算术四则运算的规则所确定的运算优先关系的规定来实现对表达式的编译或解释执行的。 • 将表达式视为由操作数、运算符、界限符(称为单词)组成。 • 操作数:常数、变量或符号常量。 • 算符:运算符、界限符
四、 栈的应用举例 • 遵循算术四则运算规则,一般任意两个相继出现的两个算符p1和p2之间的优先关系至多有下面三种之一: p1<p2 p2的优先权高于p1 p1=p2 二者优先权相等 p1>p2 p2的优先权低于p1见表3.1
四、 栈的应用举例 • 一般作为相同运算符,先出现的比后出现的优先级高;先出现的运算符优先级低于“(”,高于“)”;后出现的运算符优先级高于“(”,低于“)”;优先权相等的仅有“(”和“)”、“#”。 #:作为表达式结束符,通常在表达式之前加一“#”使之成对,当出现“#”=“#”时,表明表达式求值结束,“#”的优先级最低。
四、 栈的应用举例 (2)后缀表达式法 中缀表达式——表达式的运算符在操作数的中间。 后缀算术表达式(逆波兰式)——将运算符置两个操作数后面的算术表达式。 前缀表达式(波兰式)又称波兰式,与后缀表达式相反。 例: 将3*(x+y)/(1-x)转换为后缀表达式 3xy+*1x-/ 方法:{ [ 3 * ( x + y ) + ] * / ( 1 – x ) - } / 3xy+81x-/
四、 栈的应用举例 ①中缀表达式转换成后缀表达式 算法一: 设一个数组str存放中缀表达式,一个数组exp存放转换后的后缀表达式,栈s存放作为中间过程中不能立即送入数组的运算符,设字符“#”为转换后表达式的终止符。
四、 栈的应用举例 依次从键盘输入表达式中的字符,对于每一个c: a.若c为数字,则将c依次存入数组exp中; b.若c为“(” 则将c压入栈 s中; c.若c为“)”,则将栈 s中“(”之前的所有字符依次弹出存入数组exp中,然后将“(”弹出; d.若c为“+”或“-”,则将栈s中“(”以前的所有字符依次弹出存入数组 exp中,然后将c压入栈s中; e. 若c为“*”若“/”,则将栈s中的栈顶连续的“*”或“/”弹出并依次存入exp中,然后将c 压入栈s中; f.若c为“#”,则将栈s中的所有运算符依次弹出并存入数组exp中,然后再将c存入数组exp 中,最后可得到后缀表达式存入数组exp中。
四、 栈的应用举例 以3*(x+y)/(1-x)为例,说明转换过程。 str / ( * / ( + ) - ) exp - s * + + ( /
四、 栈的应用举例 算法二: 设一个数组str存放中缀表达式,一个数组exp存放转换 后的后缀表达式,栈s作为中间过程中不能立即送入数组的 运算符。 对于运算符也有一个优先级的比较这同(1)是相同的。 设先后出现的两个运算符为p1和p2。 a.首先在栈s中压入”#”,然后从数组str中的第一个字符开始扫描; b.当字符是数字字符时,将从该字符开始的一组数字符及附加空格,依次送入数组exp中。
四、 栈的应用举例 c.当字符是运算符p2时,则检查p2与s栈顶元素p1之间的关系,并作以下处理: 若p2>p1,则将运算符p2压入s; 若p2<p1,则将p1弹出,并送入数组exp中,然后p2继续与新的栈顶中的运算符p1比较; 若p2为右括号,则栈顶必然有一左括号,将栈顶左括号弹出(此过程正是删除一对括号),然后继续扫描下一个字符; d.重复上述过程b~c,直到扫描到str数组中的算符“#”为止。 运算结束后,exp数组中存放的是转换后的后缀表达式。
四、 栈的应用举例 以3*(x+y)/(1-x)为例,说明转换过程。 str exp s *( + ) / ( - ) # / - * + /
四、 栈的应用举例 ②后缀表达式求值 将转换后的后缀表达式运算求出结果。 接上,exp中存放带“#”的后缀表达式,栈s存放参与运算的操作数、中间结果和最后结果。 从数组的第一个字符开始扫描。 l若遇到数字字符,则将以该字符开始的一组数字(直到遇到空格为止)转换成对应的数值(即一个操作数),再压入栈s。 l若遇到的是运算符,则从栈s的栈顶依次弹出两个操作数,进行相应的运算,再将其运算结果压入栈s。 l继续扫描exp中的下一个字符,重复上述两个步骤,直到扫描到exp中的“#”为止。此时栈s中的栈顶值就是后缀表达式的值。
四、 栈的应用举例 exp 12*3=36 7+5=12 2-1=1 36/1=36
3.2 栈与递归的实现 递归是程序设计中常用的算法之一。 1. 递归:函数(过程)直接或间接调用自身。 2. 递归算法的实现离不开栈 基于函数嵌套调用的特点,即“后调用的先返回”。函数之间的信息传递和控制转移,利用“栈”来实现是最合适的。在系统运行过程中,开辟一个存储空间,每调用一个函数,为其分配一个栈顶空间保存其相关信息;每退出一个函数,就释放它的存储空间。 递归函数的运行过程类似于多个函数嵌套调,由于是调用同一个函数,就有一个“层次”的概念。 见图
3.2 栈与递归的实现 保存参数,返回地址 分配存储空间 控制转移 存入栈:存储区 f1( ) { f11(); … return; } main() { f1(); … } f11() { …. return; } f11 f1 保存结果 释放局部变量 返回到调用函数 栈 从栈中取
3.2 栈与递归的实现 3. 递归过程 为了保证递归函数正确执行,设立一个“递归工作栈”作为整个递归函数运行期间使用的数据存储区。每一层递归所需信息构成一个“工作记录”,其中包括所有的实参、局部变量以及上一层返回的地址。每进入一层递归,就产生一个新的工作记录并将其压入栈顶,每退出一层递归,就从栈顶弹出一个工作记录。当前执行层的工作记录必是递归工作栈的栈顶记录,这个记录称为“活动记录”。 见图
3.2 栈与递归的实现 保存参数,返回地址 分配存储空间 控制转移 存入栈:存储区 f1( ) { f1( ); … return; } main() { f1( ); … } f1( ) { …. return; } f1 f1 保存结果 释放局部变量 返回到调用函数 栈 从栈中取
3.2 栈与递归的实现 4. 递归的优点 应用程序易于设计; 程序结构简单精练,只需描述递归关系和终止条件,不用具体描述执行过程。 递归算法格式: if(递归终止条件) 返回结果; else 调用函数;//用递归关系将程序分割为更简单的小程序 5. 递归的缺点 程序较难理解,可读性差; 运算速度较慢而且占用较多的存储空间,递归的层次决定所消耗的时间和空间,层次越多,消耗越多。 递归终止条件 递归关系 考虑两方面:
3.2 栈与递归的实现 6. 常用的递归 (1)定义是递归的 a. 阶乘函数 b.Fibonacci数列 (2)数据结构是递归的 如:链表、二叉树、广义表等数据结构本身固有递归的特性,对于递归的数据结构,采用递归的方法编写算法特别简单。 (3)问题的解法是递归的 N皇后问题、Hannoi塔问题等,用递归求解比迭代求解更简单。
3.2 栈与递归的实现 例:运用递归设计一个将字符串反转输出的程序。 如:将“ABC”反转输出为“CBA”。 算法: 递归结束条件:每个字符都输出。 递归执行部分:从后一个字符开始输出,字符串数 组下标后移到字符串长度。 已知:字符串内容及长度。 设输入:ABC
3.2 栈与递归的实现 1 char string[30]; 2 int length; 3 Void reverse(int n) 4 {if(n<len) 5 {reverse(n+1); 6 printf(“%c”,string[n]); • } • } 9 Void main() 10 { scanf(“%s”,string); 11 len=strlen(string); 12 reverse(0); 13 printf(“\n”); 14 }
3.2 栈与递归的实现 Void main {…. reverse(0); printf(“\n”); } Void reverse(0) {if(0<3) {reverse(1); printf(“%c”,string[0]); }} 6行地址 A 13行地址 Void reverse(1) {if(1<3) {reverse(2); printf(“%c”,string[1]); }} B 6行地址 13行地址 6行地址 13行地址 6行地址 Void reverse(2) {if(2<3) {reverse(3); printf(“%c”,string[2]); }} C 6行地址 6行地址 13行地址 Void reverse(3) {if(3<3) //不满足条件 { … …; }}
3.2 栈与递归的实现 一般来说,递归并不是一种高效的方法。以计算Fibonacci数列为例,用递归方法的时间复杂度为O(2n)。 一般对尾递归或单向递归的情形,都可利用循环方法,将递归过程改为非递归过程。所谓单向递归如Fibonacci数列问题,而尾递归则是单向递归的特例。它的递归调用语句只有一个,而且是放在过程的最后。 一般从递归过程改为非递归过程的方法是先根据递归算法画出程序流程图,然后建立起循环结构。
3.2 栈与递归的实现 n n=0,1 Fib(n-1)+Fib(n-2) n>1 Fib(n)= 1 1 2 3 5 8 13 …… Long fib(long n) long fiblter(long n) {if (n<=1) return n; { long f1=0,f2=1,f; else return fib(n-1)+fib(n-2); if(n<=1) return n; } else { for(i=2;i<=n;i++) { f=f1+f2;f1=f2; f2=f;} return f; } } 1 1 2 3 5 8…... f1 f2 f f f1 f2
3.2队列 一、队列的定义 二、抽象数据类型队列的定义 三、队列的表示和实现