480 likes | 734 Views
第 3 章 栈和队列. wangzs@vip.sina.com. 学习要点. 栈及其表示 栈的应用 队列的定义 队列的链式表示 队列的顺序表示. 概 述. 栈和队列是 操作受限 的特殊的 线性表 插入、删除操作只能在表的两端进行. 插 入 删 除 线性表: Insert(L,i,x) Delete(L,i) (1≤i≤n+1) (1≤i≤n) 栈: Insert(L,n+1,x) Delete(L,n) 队列: Insert(L,n+1,x) Delete(L,1). 3.1 栈. K n-1. k 1. k 0.
E N D
第3章 栈和队列 wangzs@vip.sina.com
学习要点 • 栈及其表示 • 栈的应用 • 队列的定义 • 队列的链式表示 • 队列的顺序表示
概 述 • 栈和队列是操作受限的特殊的线性表 插入、删除操作只能在表的两端进行 插 入 删 除线性表: Insert(L,i,x)Delete(L,i)(1≤i≤n+1)(1≤i≤n)栈: Insert(L,n+1,x) Delete(L,n)队列: Insert(L,n+1,x)Delete(L,1)
3.1 栈 Kn-1 ... k1 k0 栈是限定在表的同一端进行插入或删除操作的线性表。进行插入或删除操作的一端称为栈顶,另一端称为栈底。 入栈 出栈 栈的操作是按后进先出的原则进行的。又称为后进先出(LIFO)表。 栈顶 栈底
3.1.1 栈的抽象数据类型的定义 ADT Stack{ 数据对象:D={ai|ai∈ ElemSet,i=1,2…,n,n≥ 0} 数据关系:R={<ai-1,ai>|ai-1,ai∈ D,I=2,..,n} 约定a1为栈底,an为栈顶 基本操作: InitStack(&s) DestroyStack(&s) ClearStack(&s) StackEmpty(s) StackLength(s) GetTop(s,&e) Push(&s,e) Pop(&s,&e) StackTraverse(s,visit()) }ADT STack
3.1.2 栈的表示和实现 • 栈的两种表示方法: 顺序表示(顺序栈)、链式表示(链栈) • 顺序表示:顺序存储结构 • 使用一维数组实现顺序栈在计算机中的存储 • 附设指针base,top指示栈底和栈顶位置 • 允许的最大存储空间
顺序栈的表示 S.base S.top S.stacksize • 顺序栈结构定义: • #define STACK_INIT_SIZE 100; • //存储空间初始分配量 • typedef struct{ • SElemType *base; • SElemType *top; • int stacksize; • }SqStack;
顺序栈的实现 基本操作函数原型说明: Status InitStack(SqStack &S); Status DestoryStack(SqStack &S); Status ClearStack(SqStack &S); Status StackEmpty(SqStack S); int StackLength(SqStack S); Status GetTop(SqStack S,SElemType &e); Status Push(SqStack &S,SElemType e); Status Pop(SqStack &S,SElemType &e); Status StackTraverse(SqStack S,Status(*visit()));
基本操作的算法描述 构造一个空栈S: Status InitStack(SqStack &S) { S.base=(SElemType*) malloc (STACK_INIT_SIZE*sizeof(SElemType)); if (!S.base) exit(OVERFLOW); S.top=S.base; S.stacksize= STACK_INIT_SIZE ; return OK; }//InitStack
基本操作的算法描述 取栈顶元素: Status GetTop(SqStack S,SElemType &e) { if(S.top==S.base) return ERROR; e=*(S.top-1); return OK; }//GetTop … … a1 a2 an
基本操作的算法描述 入栈: Status Push(SqStack &S,SELemType e) { if(S.top-S.base>=S.stacksize) //栈满 return ERROR; *S.top++=e; return OK; }//Push … … a1 a2 an e
基本操作的算法描述 出栈S: Status Pop(SqStack &S,SELemType &e) { if(S.top==S.base) return ERROR; e=*--S.top; return OK; }//Pop
链式表示—链栈 栈顶 栈底 S.top 结构定义:typedef struct { SLink top; int length; } Stack; ^
3.2 栈的应用 例1: 数制转换,将一个十进制数转换成一个八进制数。 void conversion(){ InitStack(S); scanf(“%d”,N); while (N){ push(S,N % 8); N=N/8; } while (!StackEmpty(S)){ Pop(S,e); printf(“%d”,e); } }//conversion 分析:需要先保存在计算过程中得到的八进制数的各位,然后逆序输出,按"后进先出"的规律进行的
例2: 括号匹配的检验 栈的应用 例如:[ ( [ ] [ ] ) ] 1 2 3 4 5 6 7 8 算法策略: 1、对每一个左括号,都要等待和它相匹配的右括号出现; 2、所有等待的左括号排成一队等待,且后出现的排在前; 3、每个右括号看是否有排在第一位的左括号配对。 解题思想: 1、遇到左括号,入栈; 2、遇到右括号,判断栈是否为空,空,错误,不空,看是否匹配; 3、在初始化空栈前提下,在检查程序结束前,栈应为空
栈的应用 例3:行编辑程序 行编辑程序的功能:接受用户从终端输入的程序或数据,并存入用户的数据区。 较好的做法:设立一个输入缓冲区,用来接收用户输入的一行字符,然后逐行存入用户数据区 例如:退格符“#”:表示前一个字符无效 退行符“@”:表示当前行中的字符均无效 则:whli# #ilr#e(s#*s) outcha@putchar(*s=#++); 实际有效的行: while(*s) putchar(*s++)
栈的应用 算法设计:使用栈结构实现输入缓冲区 从终端接收一个字符进行判断: 1、不是“#”也不是“@”则压入栈顶 2、是“#”,则栈顶元素出栈 3、是“@”,则栈清空 4、接收到行结束符或全文结束符将数据送入用户数据区
void LineEdit(){ //利用字符栈S作为输入缓冲区 InitStack(S); Ch=getchar(); While(ch!=EOF){ //EOF为全文结束符 while(ch!=EOF && ch!=‘\n’){ switch(ch){ case ‘#’:Pop(S,c); break; case ‘@’:ClearStack(S); break; default:Push(S,ch); } ch=getchar(); } 将栈内字符传送到调用此过程的数据区 ClearStack(S); if(ch!=EOF) ch=getchar(); } DestroyStack(S); }//lineEdit
栈的应用 例4: 迷宫求解问题 入口 出口 (a) 迷宫的图形表示 (b) 迷宫的二维数组表示
栈的应用 未曾走到过的通道 求解迷宫中一条路径的方法: 从入口出发,沿某一方向进行探索,若能走通,则继续向前走;否则沿原路返回,换一方向再进行探索,直到所有可能的通路都探索到或者到达出口为止。这类方法统称回溯法。 用一个栈记录走过的位置,栈中每个元素包括三项,分别记录当前位置的行坐标、列坐标以及在该位置上所选的方向
栈的应用 讨论在某一位置进行试探,设迷宫中任一位置(i, j) N (i-1,j) w (i,j-1) (i, j) E (i,j+1) S (i+1,j)
栈的应用 把入口压入栈中; while 栈不空时 { 取栈顶元素; while 存在试探方向 { 求下一个方块位置maze[g][h]; if (maze[g][h]是出口块) {打印路径上的每一个方块;return; } if(maze[g][h]= =0) /*可前进*/ {标记maze[g][h];(g,h,方向)进栈; 前进到下一个位置; } } }
栈的应用 例5: 表达式求值——算符优先法 3*(7+3*6/2-5) =3*(7+18/2-5) =3*(7+9-5) =3*(16-5) =3*11 =33 前一个运算符优先级大,运算! 任何一个表达式都是由操作数(operand)、 运算符(operator)和界限符(delimiter)组成—单词
优先关系表 结束符 op2 + op1 不能比较
输入: 3 * ( 7 + 3 * 6 / 2 - 5 ) # 实现这个转换的基本思想是: 1、设置两个栈 OPTR,OPND 2、逐个接收字符 3、运算符θ1与θ2之间的关系: θ1栈顶运算符, θ2当前读到的运算符 > :θ1出栈,OPND栈顶两个元素出栈 <: θ2入栈 =:θ1出栈 4、# 与 # 相遇,运算结束
InitStack(OPTR); Push(OPTR,’#’); InitStack(OPND); c=getchar(); while (c!=‘#’ || GetTop(OPTR)!=‘#’){ if(!In(c,OP)){ Push(OPND,c);c=getchar(); } else switch( Precede(GetTop(OPTR),c) ){ case ‘<‘ : Push(OPTR,c); c=getchar(); break; case’=’ : Pop(OPTR,x); c=getchar(); break; case ‘>‘ : Pop(OPTR,theta); Pop(OPND,b); Pop(OPND,a); Push(OPND,Operate(a,theta,b)); break; } }// while return GetTop(OPND); }// EvaluateExpression
3.3 栈与递归 1. 阶乘函数 n! 1 n=0 n*F(n-1) n>0 F(n)= 2. Fibonacci 数列 0 n=0 Fib(n)= 1 n=1 Fib(n-1)+Fib(n-2) 其他 3.Hanoi问题
栈与递归 阶乘函数 n! 1 n=0 n*F(n-1) n>0 F(n)= • 递归函数的定义 一个函数直接或者间接地调用了自己。 一个递归定义必须一步比一步简单,最后有终结. 在n阶乘的定义中,当n为0时定义为1,它不再用递归来定义,称为递归定义的出口,简称为递归出口。
栈与递归 • 函数的嵌套调用 函数嵌套调用时,按照“后调用先返回”的原则进行。 int main() { int m,n; ... first(m,n); 1: … } int first(int s, int t) { int i; … second(i); 2: … } int second(int d) { int x,y; 3: … }
栈与递归 调用前: (1)将所有的实参、返回地址传递给被调用函数; (2)为被调用函数的局部变量分配存储区; (3)将控制转移到被调用函数的入口。 调用后返回: (1)保存被调用函数的计算结果; (2)释放被调用函数存储区存放的数据; (3)依照被调用函数保存的返回地址将控制转移到调用函数。
栈与递归 • 递归工作栈:递归过程执行过程中所占用的数据区 • 将递归调用时的实在参数和函数返回地址传递给下一层执行的递归函数; • 保存本层的参数和局部变量,以便从下一层返回时重新使用它们。 • 递归工作记录 每一层的递归参数等数据
栈与递归 void hanoi (int n,char x,char y,char z) 1 { 2 if (n= =1) 3 move(x,1,z); 4 else{ 5 hanoi(n-1,x,z,y); 6 move(x,n,z); 7 hanoi(n-1,y,x,z); 8 } 9 } Main(){ 0 hanoi(3,a,b,c) }
队列 进队 出队 k1 k2 k3 …. kn-1 队头 队尾 3.4 队列 队列也是一种特殊的线性表,对于它所有的插入都在表的一端进行,所有的删除都在表的另一端进行。进行删除的这一端叫队列的头,进行插入的这一端叫队列的尾。当队列中没有任何元素时,称为空队列。队列也称作先进先出表。
3.4.1 队列的抽象数据类型定义 • ADT Queue{ • 数据对象:D={ai|ai∈ ElemSet,i=1,2…,n,n≥ 0} • 数据关系:R1={<ai-1,ai>|ai-1,ai∈ D,i=2,..,n} • 约定a1端为队列头, an端为队列尾。 • 基本操作: • InitQueue (&Q) • DestroyQueue(&Q) • ClearQueue(&Q) QueueEmpty (&Q) • QueueLength(Q)GetHead(Q,&e) • EnQueue (&Q,e ) • DeQueue (&Q,&e) • QueueTraverse(Q,visit( ))}ADT Queue
Q.front …. k0 k1 k2 kn-1 Q.rear 3.4.2 链队列--队列的链式表示和实现 Q.front 如何判队列为空? 头结点 Q.rear typedef Struct QNode{ QElemtype data; struct Qnode *next; }QNode,*QueuePtr; typedef struct{ QueuePtr front; QueuePtr rear; }LinkQueue;
链队列的基本操作算法描述 Status InitQueue(LinkQueue &Q){ Q.front=Q.rear=(QueuePtr) malloc (sizeof(QNode)) if(!Q.front) exit (OVERFLOW); Q.front->next=NULL; return OK; } //InitQueue
链队列的基本操作算法描述 Status DestroyQueue(LinkQueue &Q){ while (Q.front){ Q.rear=Q.front->next; free(Q.front); Q.front=Q.rear; } return OK; }
链队列的基本操作算法描述 Status EnQueue(LinkQueue &Q,QElemtype e){ p=(QueuePtr)malloc(sizeof(QNode)); if (!p) exit(OVERFLOW); p->data=e; p->next=NULL; Q.rear->next=p; Q.rear=p; return OK; }
链队列的基本操作算法描述 Status DEQueue(LinkQueue &Q,QElemtype &e){ if(Q.rear= = Q.front ) return ERROR; p=Q.front->next; e=p->data; Q.front->next=p->next; if (Q.rear= =p) Q.rear=Q.front; free(p); return OK; }
0 1 2 3 4 5 6 7 Q.front b c d e d e f g Q.front Q.rear 队头指针 队尾指针 队尾指针 队头指针 k1 Q.rear k2 k3 k4 k7 k5 k6 3.4.3.队列的顺序存储结构--循环队列 0 1 2 3 4 5 6 7 图(a) 空队列 图(b) 队列满,判断
队列的顺序存储结构--循环队列 #define MAXQSIZE 100 typedef Struct{ QElemtype *base; int front; int rear; }SqQueue;
循环队列的基本操作算法描述 Status InitQueue(SqQueue &Q){ Q.base=(QElemType*)malloc(MAXQSIZE* sizeof(QElemType)); if(!Q.base) exit (OVERFLOW); Q.front=Q.rear=0; return OK; } //InitQueue
循环队列的基本操作算法描述 int QueueLength(SqQueue Q){ return (Q.rear-Q.front+MAXQSIZE) % MAXQSIZE; } Status EnQueue(SqQueue &Q,QElemtype e){ if((Q.rear+1)% MAXQSIZE= = Q.front ) return ERROR; Q.base[Q.rear]=e; Q.rear=(Q.rear+1)% MAXQSIZE; return OK; }
循环队列的基本操作算法描述 Status DEQueue(SqQueue &Q,Qelemtype e){ if(Q.rear= = Q.front ) return ERROR; e= Q.base[Q.front]; Q.front=(Q.front+1)% MAXQSIZE; return OK; }
小结 1、本章主要讨论了:栈和队列的基本概念、 存储结构、基本运算以及应用实例。 2、对于栈来说,它的插入和删除都是在表的同 一端进行的。 采用顺序存储结构时,要注意栈满、栈空的条件; 用链式存储结构时,要注意链的方向。 3、对队列来说,它的插入运算在表的一端进行, 而删除运算却在表的另一端进行。使用循环队列 实现顺序存储结构,这样可解决假溢出问题,但 要特别注意队列满和队列空的条件及描述。
小结 4、递归问题是计算机科学中的一个重要问题, 递归调用时,要用栈存储调用前的状态。
作业及思考题 A.简述栈和队列的相同点和不同点. B.假设以带头结点的循环链表表示队列,并且只设一个指针指向队尾元素结点,编写队列的初始化、入队列、出队列的算法。
实验题 1. 将任意的十进制数转换为d进制数; 2.判断含()、{}、[]、的算术表达式中所含括号是否正确配对; 3.判断以@为结束符的字符序列是否为回文,所谓回文是指正读和反读都相同的字符序列,如abcdcba或abcba是回文; 4.实现行编辑程序; 5.删除链栈中所有满足条件的元素; 6.表达式求值问题; 7.谜宫求解问题。