980 likes | 1.26k Views
第三章 栈和队列. 教学内容 1 、栈和队列的定义及特点; 2 、栈的顺序存储表示和链接存储表示; 3 、队列的顺序存储表示和链接存储表示; 4 、栈和队列的应用举例。. 第三章 栈和队列. 教学要求 1 、掌握栈和队列的定义、特性,并能正确应用它们解决实际问题; 2 、熟练掌握栈的顺序表示、链表表示以及相应操作的实现。特别注意栈空和栈满的条件; 3 、熟练掌握队列的顺序表示、链表表示以及相应操作的实现。特别是循环队列中队头与队尾指针的变化情况。. 第三章 栈和队列. 3.1 栈 3.2 栈的应用举例 3.3 栈与递归的实现
E N D
第三章 栈和队列 • 教学内容 1、栈和队列的定义及特点; 2、栈的顺序存储表示和链接存储表示; 3、队列的顺序存储表示和链接存储表示; 4、栈和队列的应用举例。
第三章 栈和队列 • 教学要求 1、掌握栈和队列的定义、特性,并能正确应用它们解决实际问题; 2、熟练掌握栈的顺序表示、链表表示以及相应操作的实现。特别注意栈空和栈满的条件; 3、熟练掌握队列的顺序表示、链表表示以及相应操作的实现。特别是循环队列中队头与队尾指针的变化情况。
第三章 栈和队列 3.1 栈 3.2 栈的应用举例 3.3 栈与递归的实现 3.4 队列
一、栈的定义及特点 1、栈的定义: 限定仅在表尾进行插入或删除操作的线性表,因此,对栈来说,表尾端有其特殊含义,表尾—栈顶(top),表头—栈底(bottom),不含元素的空表称空栈。 进栈 出栈 ... 栈顶 an ……... 栈s=(a1,a2,……,an) a2 栈底 a1 3.1 抽象数据类型栈的定义 允许插入(入栈)、删除(出栈)的一端叫栈顶 不允许插入、删除的一端叫栈底
2、栈的特点 最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。 即,栈是一种后进先出(Last In First Out)的线性表,简称为LIFO表。(先进后出(FILO)) 3、栈的例子及用途 例一:放在地板上的一叠书,只能从顶上面拿走一本,若再加入一本,也只能放在顶上。 例二:子弹的压入、发射。 例三:盘子的刷洗及使用。 例四:走入死胡同的人。 例五:函数的嵌套调用值的返回。
4、栈的抽象数据类型的定义: ADT Stack{ 数据对象:D={ ai | ai∈Elemset , i =1,2,…,n, n>=0} 数据关系:R1={< ai-1, ai > | ai-1,ai ∈ D, i = 1,2,…,n} 约定an端为栈顶, a1端为栈底。 基本操作: InitStack(&S)//初始化栈 操作结果:构造一个空栈s (不含任何元素)。
DestroyStack(&S) 初始条件:栈s已存在。 操作结果:栈s被销毁。 ClearStack(&S) 初始条件:栈s已存在。 操作结果:将s清为空栈。 StackEmpty(S) //判栈空 初始条件:栈s已存在。 操作结果:若栈S为空栈,则返回TRUE, 否则FALSE。
StackLength(S) 初始条件:栈s已存在。 操作结果:返回s的元素个数,即栈的长度。 GetTop(S,&e) //取栈顶元素 初始条件:栈s已存在且非空。 操作结果:用e返回s的栈顶元素。 Push(&S,e) //进栈 初始条件:栈s已存在。 操作结果:插入元素e为新的栈顶元素。 “入栈”、 “插入”、 “压入”
Pop(&S,&e) //出栈 初始条件:栈s已存在且非空。 操作结果:删除s的栈顶元素,并用e返回其值。 也称为“退栈”、 “删除”、 “弹出”。 StackTraverse(S,visit()) 初始条件:栈s已存在且非空。 操作结果:从栈底到栈顶依次对s的每个数据元素调用函数visit()。一旦visit()失败,则操作失效。 } ADT Stack
5、操作考点示例: 例1: 对于一个栈,给出输入项A、B、C,如果输入项序列 由ABC组成,试给出所有可能的输出序列。 栈状态 产生式 A进 A出 B进 B出 C进 C出 ABC A进 A出 B进 C进 C出 B出 ACB A进 B进 B出 A出 C进 C出 BAC A进 B进 B出 C进 C出 A出 BCA A进 B进 C进 C出 B出 A出 CBA 不可能产生输出序列CAB
例2:一个栈的输入序列是12345,若在入栈的过程中允许出栈,则栈的输出序列43512可能实现吗?12345的输出呢?例2:一个栈的输入序列是12345,若在入栈的过程中允许出栈,则栈的输出序列43512可能实现吗?12345的输出呢? 答: 43512不可能实现,主要是其中的12顺序不能实现; 12345的输出可以实现. 例3:如果一个栈的输入序列为123456,能否得到435612和135426的出栈序列? 435612中到了12顺序不能实现; 135426可以实现。 答:
例4已知一个栈的进栈序列是1,2,3,…,n,其输出序列是p1,p2,…,pn,若p1=n,则pi的值。例4已知一个栈的进栈序列是1,2,3,…,n,其输出序列是p1,p2,…,pn,若p1=n,则pi的值。 (A) i (B) n-i (C) n-i+1 (D) 不确定 答:当p1=n时,输出序列必是n,n-1,…,3,2,1,则有: p2=n-1, p3=n-2, …, pn=1 推断出pi=n-i+1,所以本题答案为C。
例5设n个元素进栈序列是1,2,3,…,n,其输出序列是p1,p2,…,pn,若p1=3,则p2的值。例5设n个元素进栈序列是1,2,3,…,n,其输出序列是p1,p2,…,pn,若p1=3,则p2的值。 (A) 一定是2 (B) 一定是1 (C) 不可能是1 (D) 以上都不对 答:当p1=3时,说明1,2,3先进栈,立即出栈3,然后可能出栈,即为2,也可能4或后面的元素进栈,再出栈。因此,p2可能是2,也可能是4,…,n,但一定不能是1。所以本题答案为C。
a1 a2 a3 a4 … base top 二、栈的顺序表示和实现(顺序栈) 利用一组地址连续的存贮单元依次自栈底到栈顶存放栈的数据元素。栈底元素是最先进入的,实际上是线性表的第一个元素。 //动态分配空间(表示方法1) typedef struct { SElemType *Base;// 栈底指针 int top;// 栈顶指针 int StackSize;//当前已分配的存储空间,以元素为单位。 }SqStack; 注意:栈空和栈满条件! top==-1 和 top>=S.StackSize-1
// 动态分配顺序栈空间(表示方法2) #define STACK_INIT_SIZE 100 #define STACKINCREMENT 10 typedef struct {SElemType *Base; // 栈底指针 SElemType *Top;// 栈顶指针 int StackSize; //当前已分配的存储空间,以元素为单位 }SqStack;
a1 a2 a3 a4 … base top 静态分配顺序栈空间表示 #define MaxSize 100 typedef struct { ElemType base[MaxSize]; // base为栈底指针 int top;// top栈顶指针 }SqStack;//顺序栈类型定义 注意:栈空和栈满条件! top==-1 和 top>=MaxSize-1
A top top base base 空栈 A进栈 • s.top=s.base 表示空栈; • s.base=NULL表示栈不存在; • 当插入新的栈顶元素时,先在栈顶指针位置放入新元素,然后栈顶指针s.top++; • 删除栈顶元素时,栈顶指针s.top- -,然后再删除; • 当s.top-s.base>=stacksize时,栈满,溢出
top E D top C top B B top base base base A A A base 顺序栈的图示 入栈: if( 没有空间) { //realloc,调整top } *top = A; top ++; 出栈: if( top != base ) { top --; e = *top; } 空栈: base = top;
顺序栈基本操作算法描述 1、构造一个空栈 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
2、取栈顶元素 Status GetTop( SqStack S, SElemType &e) {// 用e返回栈S的栈顶元素,若栈空,函数返回ERROR if( s.top != s.base ) // 栈空吗? { e = *( s.top – 1 ); return OK; } else return ERROR; }// GetTop
3、入栈操作 Status Push(SqStack &s, SElemType e ) {//把元素e入栈 if( s.top == s.base + s.stacksize ) // 若栈满,追加存贮空间 { p = (SElemType *)realloc( s.base, (s.stacksize + STACKINCREMENT)*sizeof(SElemType)); if( !p ) exit( OVERFLOW); s.base = p; s.top = s.base + s.stacksize; s.stacksize += STACKINCREMENT; }
*s.top = e; s.top++; return OK; }// Push
4、出栈操作 Status Pop( SqStack &S, SElemType &e ) {// 出栈 if( s.top == s.base ) // 空吗? { return ERROR; } s.top --; e = *s.top; return OK; }// Pop
栈的共享存储单元 有时,一个程序设计中,需要使用多个同一类型的栈,这时候,可能会产生一个栈空间过小,容量发生溢出,而另一个栈空间过大,造成大量存储单元浪费的现象。 为了充分利用各个栈的存储空间,这时可以采用多个栈共享存储单元,即给多个栈分配一个足够大的存储空间,让多个栈实现存储空间优势互补。
an an-1 a1 三、栈的链式存储表示(链栈) 栈的链式存储结构称为链栈,它的运算是受限的单链表,插入和删除操作仅限制在表头位置上进行。
链栈特点: • 链式栈无栈满问题,空间可扩充 • 链式栈的栈顶在链头
栈的链式存储结构定义 typedef struct SNode { ElemType data; struct SNode *next; }SNode,*LinkStack;
链栈的进栈算法 int Push (LinkStack &s, ElemType e) { SNode *p; p=(SNode *)malloc(sizeof(SNode)); if(!p) exit(-2); p->data=e; p->next=s->next; s->next=p; return 1; }
链栈的出栈算法 int Pop (LinkStack &s, ElemType &e) { SNode *p; if (s->next==NULL) return 0; p=s->next; e=p->data; s->next=p->next; free(p); return 1; }
作业 1、有5个元素,其进栈次序为A、B、C、D、E,在各种可能的出栈次序中,以元素C、D最先出栈(即C第一且D第二个出栈)的次序有哪几个? 2、假设以I和O分别表示进栈和出栈操作,栈的初态和终态均为空,进栈和出栈的操作序列可表示为仅由I和O组成的序列。 (1)下面所示的序列中哪些是合法的? A. IOIIOIOO B. IOOIOIIO C. IIIOIOIO D. IIIOOIOO (2)通过对(1)的分析,写出一个算法判定所给的操作序列是否合法。若合法返回1;否则返回0(假设被判定的操作序列已存入一维数组中)。 3、假设表达式中允许包含三种括号:圆括号、方括号和大括号。编写一个算法,判定表达式中的括号是否正确配对。
第三章 栈和队列 3.1 栈 3.2 栈的应用举例 3.3 栈与递归的实现 3.4 队列
3.2 栈的应用举例 一、 数制转换 二、 括号匹配的检验 三、 行编辑程序问题 四、 表达式求值 五、 迷宫求解 六、 实现递归
3.2 栈的应用 例:读入一个有限大小的整数n,并读入n个整数,然后按输入次序的相反次序输出各元素的值。 void read_write() { stack S; int n,x; printf(”Please input num int n”); scanf(“%d”, &n);//输入元素个数 init_stack(S);//初始化栈 for (i=1; i<=n; i++) {scanf(“%d” ,&x) ;//读入一个数 push_stack(S,x);//入栈 } while (!stack_empty(S)) { pop_stacks (S,x);//退栈 Printf(“%d”,x);//输出 } }
一、数制转换 例如 (1348)10=(2504)8, 其运算过程如下: n(被除数) n div 8(商) n mod 8(余数) 1348 168 4 168 21 0 21 2 5 2 0 2 0 计算顺序 输出顺序
算法分析: 由于十进制转换为其他进制数的规则,可知,求得的余数的顺序为由低位到高位,而输出则是由高位到低位。 因此,这正好利用栈的特点,先将求得的余数依次入栈,输出时,再将栈中的数据出栈。
void conversion () { stack S; InitStack(S); // 构造空栈 scanf ("%d",&N); //输入一个十进制数 while (N) { Push(S, N % 8); // "余数"入栈 N = N/8; // 非零“商”继续运算 } while (!StackEmpty(S)) { Pop(S,e); //输出八进制的各位数 printf ( "%d", e ); } } // conversion
二、括号匹配的检验 在表达式中 ([]())或[([ ][ ])] 等为正确的格式, ([]( ) 或(()))或 [( ])均为不正确的格式。 则 检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。
算法的设计思想: 读入表达式 1)凡出现左括弧,则进栈; 2)凡出现右括弧,首先检查栈是否空, 若栈空,则表明该“右括弧”多余, 否则和栈顶元素比较, 若相匹配,则“左括弧出栈” , 否则表明不匹配。 3)表达式检验结束时, 若栈空,则表明表达式中匹配正确, 否则表明“左括弧”有余。
三、行编辑程序问题 并不恰当! “每接受一个字符即存入存储器” ? 在用户输入一行的过程中,允许用户输入出差错,并在发现有误时可以及时更正。 合理的作法是: 设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区,并假设“#”为退格符,“@”为退行符。
算法的设计思想: 可设这个输入缓冲区为一个栈结构,每当从终端接受了一个字符之后先作如下判断: 1、既不是退格也不是退行符,则将该字符压入栈顶; 2、如果是一个退格符,则从栈顶删去一个字符; 3、如果是一个退行符,则将字符栈清为空栈 。
四、表达式求值 表达式求值是程序设计语言编译的一个最基本的问题。它的实现是栈应用的又一典型例子。 仅以算术表达式为例。 1、算术表达式的组成: 将表达式视为由操作数、运算符、界限符组成。 操作数:常数、变量或符号常量。 算符:运算符、界限符
2、算术表达式的形式 • 中缀(infix)表达式——表达式的运算符在操作数的中间。<操作数><操作符><操作数> 例:A*B 例:5+9*7 • 后缀(postfix)算术表达式(逆波兰式)——将运算符置两个操作数后面的算术表达式。 <操作数> <操作数><操作符> 例:AB* 例:5 9 7*+ • 前缀(prefix)表达式(波兰式),与后缀表达式相反,把运算符放在两个运算数的前面。 <操作符><操作数> <操作数> 例:*AB 例:+5*9 7
3、介绍中缀算术表达式的求值 例如:3*(7 – 2 ) (1)算术四则运算的规则: a. 从左算到右 b. 先乘除,后加减 c. 先括号内,后括号外 由此,此表达式的计算顺序为: 3*(7 – 2 )= 3 * 5 = 15
(2)根据上述三条运算规则,在运算的每一步中,对任意相继出现的算符1和2,都要比较优先权关系。(2)根据上述三条运算规则,在运算的每一步中,对任意相继出现的算符1和2,都要比较优先权关系。 一般任意两个相继出现的两个算符 1和 2之间的优先关系至多有下面三种之一: 1< 22的优先权高于 1 1= 2二者优先权相等 1> 2 2的优先权低于 1 算符优先法所依据的算符间的优先关系见教材。
θ1<θ2,表明不能进行θ1 的运算,θ2 入栈,读 下一字符。 θ1>θ2,表明可以进行θ1 的运算,θ1退栈,运算,运算结果入栈。 θ1=θ2,脱括号,读下一字符或表达式结束。
例如:3*(7 – 2 ) • 一般作为相同运算符,先出现的比后出现的优先级高; • 先出现的运算符优先级低于“(”,高于“)”; • 后出现的运算符优先级高于“(”,低于“)”;优先权相等的仅有“(”和“)”、“#”。 • #:作为表达式结束符,通常在表达式之前加一“#”使之成对,当出现“#”=“#”时,表明表达式求值结束,“#”的优先级最低。
(3)算法思想: • 设定两栈:操作符栈 OPTR ,操作数栈 OPND • 栈初始化:设操作数栈 OPND 为空;操作符栈 OPTR 的栈底元素为表达式起始符 ‘#’; • 依次读入字符:是操作数则入OPND栈,是操作符则要判断: if 操作符 < 栈顶元素,则退栈、计算,结果压入OPND栈; 操作符 = 栈顶元素且不为‘#’,脱括号(弹出左括号); 操作符 > 栈顶元素,压入OPTR栈。
定义运算符栈S 和操作数栈O 扫描基本符号 是否操作数? Y N 操作数 入栈 栈顶运算符低 于当前运算符? N Y 取出S栈顶运算符和 O栈顶的两个操作数 运算,结果入栈O 运算符 入栈 Y N 栈S为空? Y 结束
OPTR OPND INPUT OPERATE # 3*(7-2)# Push(opnd,’3’) # 3 *(7-2)# Push(optr,’*’) #,* 3 (7-2)# Push(optr,’(’) #,*,( 3 7-2)# Push(opnd,’7’) #,*,( 3,7 -2)# Push(optr,’-’) #,*,(,- 3,7 2)# Push(opnd,’2’) )# #,*,(,- 3,7,2 Operate(7-2) #,*,( 3,5 )# Pop(optr) #,* 3,5 # Operate(3*5) # 15 # GetTop(opnd) 例:3*(7-2)
4、计算后缀表达式 (1)为什么要把中缀表示法变为后缀表示法? 计算机计算表达式是机械执行,只能从左至右扫描。计算中缀表达式比较困难。 后缀表达式的求值过程是自左至右运算符出现的次序和真正的计算次序是完全一样的。 顺序扫描表达式的每一项,根据它的类型做如下相应操作: 若该项是操作数,则将其压栈; 若该项是操作符<op>,则连续从栈中退出两个操作数Y和X,形成运算指令X<op>Y,并将计算结果重新压栈。 表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。 人们仍然使用中缀表达式 ,而让机器把它们转换成后缀表达式。