430 likes | 566 Views
第三章 栈和队列. 数据结构. 栈. 3.1. 栈的应用举例. 3.2. 队列. 3.4. 内容提要. 基本 要求. 重点. 难点. 基本要求、重点、难点 3. ( 1 )掌握栈的定义和基本运算和存储结构。 ( 2 )掌握队列的定义和运算和存储结构 ( 3 )掌握栈和队列的实现方法,并能使用栈和队列解决实际问题。. 栈和队列的特点,在两种存储结构上栈的基本操作的实现,循环队列和链队列的基本运算,递归算法执行过程中栈状态的变化过程。. 两种存储结构上栈的基本操作的算法实现,循环队列和链队列的基本运算。. 3.1 栈.
E N D
第三章 栈和队列 数据结构
栈 3.1 栈的应用举例 3.2 队列 3.4 内容提要
基本 要求 重点 难点 基本要求、重点、难点3 (1)掌握栈的定义和基本运算和存储结构。 (2)掌握队列的定义和运算和存储结构 (3)掌握栈和队列的实现方法,并能使用栈和队列解决实际问题。 栈和队列的特点,在两种存储结构上栈的基本操作的实现,循环队列和链队列的基本运算,递归算法执行过程中栈状态的变化过程。 两种存储结构上栈的基本操作的算法实现,循环队列和链队列的基本运算。
3.1 栈 栈 是仅在表尾进行插入、删除操作的线性表。 表尾(即 an 端)称为栈顶/top ; 表头(即 a1 端)称为栈底/base 例如: 栈 S= (a1 , a2 , a3 , ……….,an-1 , an) a1称为栈底元素 an称为栈顶元素 插入元素到栈顶的操作,称为入栈。 从栈顶删除最后一个元素的操作,称为出栈。 强调:插入和删除都只能在表的一端(栈顶)进行! 想一想:要从栈中取出a1,应当如何操作?
问题1:堆栈是什么?它与一般线性表有什么不同?问题1:堆栈是什么?它与一般线性表有什么不同? 堆栈是一种特殊的线性表,它只能在表的一端(即栈顶)进行插入和删除运算。 一般线性表 堆栈 逻辑结构:1:1 逻辑结构: 1:1 存储结构:顺序表、链表 存储结构:顺序栈、链栈 运算规则:随机存取 运算规则:后进先出(LIFO) “进”=插入=压入=PUSH(an+1) “出”=删除=弹出=POP(an)
an+1 顺序栈S 栈顶top 高地址 an …… ai …… a2 栈底base a1 低地址 栈不存在的条件: base=NULL; 栈为空 的条件 : base=top; 栈满的条件 : top-base=stacksize;
问题:什么叫“向上生成”的栈?“向下生成” 是如何生成的? 若入栈动作使地址向高端增长,称为“向上生成”的栈; 若入栈动作使地址向低端增长,称为“向下生成”的栈; 对于向上生成的堆栈: 入栈口诀:堆栈指针top “先压后加” : S[top++]=an+1 出栈口诀:堆栈指针top “先减后弹” : e=S[--top]
问题3: 为什么要设计堆栈? 它有什么独特用途? • 调用函数或子程序非它莫属; • 递归运算的有力工具; • 用于保护现场和恢复现场; • 简化了程序设计的问题。 下面用3个例子来帮助理解堆栈:
例1.一个栈的输入序列为1,2,3,若在入栈的过程中允许出栈,则可能得到的出栈序列是什么?例1.一个栈的输入序列为1,2,3,若在入栈的过程中允许出栈,则可能得到的出栈序列是什么? 答: 可以通过穷举所有可能性来求解: ① 1入1出, 2入2出,3入3出, 即123; ② 1入1出, 2、3入,3、2出, 即132; ③ 1、2入,2出, 3入3出, 即231; ④ 1、2入,2、1出,3入3出, 即213; ⑤ 1、2、3入,3、2、1出, 即321; 合计有5种可能性。
例2:一个栈的输入序列是12345,若在入栈的过程中允许出栈,则栈的输出序列43512可能实现吗?12345的输出呢?例2:一个栈的输入序列是12345,若在入栈的过程中允许出栈,则栈的输出序列43512可能实现吗?12345的输出呢? 答: 43512不可能实现,主要是其中的12顺序不能实现; 12345的输出可以实现,每压入一数便立即弹出即。 思考:有无通用的判别原则?
例3:设依次进入一个栈的元素序列为c,a,b,d,例3:设依次进入一个栈的元素序列为c,a,b,d, • 则可得到出栈的元素序列是: • A)a,b,c,d B)c,d,a,b • C)b,c,d,a D)a,c,d,b 答: A)、D)可以, B)、C)不行。 讨论:有无通用的判别原则? 有!若输入序列 …,Pj…Pk…Pi …(Pj<Pk<Pi), 一定不存在这样的输出序列…,Pi…Pj…Pk … 即对于输入序列1,2,3,不存在输出序列3,1,2
本节重点:顺序栈和链栈的基本操作 栈的抽象数据类型定义: ADT Stack{ 数据对象:D=…… 数据关系:R=…… 基本操作: …… } ADT Stack 入栈、出栈、 建栈初始化、 判断栈满或栈空、 读栈顶元素值等。
动态数组 顺序栈的存储表示: #define STACK-INIT-SIZE 100//存储空间初始分配量 #define STACKINCREMENT 10//存储空间分配增量 typedef struct{ SElemType *base;//栈的基址即栈底指针 SElemType *top;//栈顶指针 int stacksize;//当前分配的空间 }SqStack;
top top top top top 顺序栈的入栈操作——例如用堆栈存放(A,B,C,D) 高地址M A B A C B A D C B A 低地址L 核心语句: top=L; 顺序栈入栈函数PUSH() status Push(ElemType e) { if(top>M){上溢} else s[top++]=e; } Push (A); Push (B); Push (C); Push (D);
高地址M D C B A D C A D C B A D C B A D B top top top top 低地址L 顺序栈出栈操作——例如从栈中取出‘B’ 核心语句: Pop ( ); 顺序栈出栈函数POP() status Pop( ) { if(top=L) { 下溢 } else { e=s[--top]; return(e);} } Pop ( ); Printf( Pop () );
data next st a n a n-1 a 2 a 1 链栈的入栈操作和出栈操作(教材省略) 栈也可以用链式结构来表示,用链式结构来表示的栈就是链栈 (1) 链栈的构造方式——以头指针为栈顶,在头指针处插入或删除. 链栈中每个结点由两个域构成: data域和next域,其定义为: 栈顶 typedef Struct SNode{ SElemType data; Struct SNode * next; } Node; 栈底 Node *st, *p; int m=sizeof(Node);
(2) 操作 Push (SElemType e) { p=(Node*)malloc(m); if(!p){上溢} else{ p->data=e; p->link=st; st=p;} } 插入表头 链栈 入栈函数 Status Pop( ) { if(st==NULL){下溢} else{e=st->data;p=st;st=st->link; free(p); return(e);} } 从表头删除 链栈 出栈函数
3.2 栈的应用举例 例1表达式求值( 这是栈应用的典型例子 ) 这里,表达式求值的算法是 “算符优先法”。 例如:编写算法,用栈实现表达式3*(7 – 2 )求值。 一个算术表达式是由 操作数(x,y,z…)和 算符(* ,/, + ,-,(,),# )组成. 表达式的起止符号 (1)表达式求值必须满足算术四则运算规则: a. 从左算到右 b. 先乘除,后加减 c. 先括号内,后括号外
为了实现算符优先算法,可以设定两个工作栈,为了实现算符优先算法,可以设定两个工作栈, OPND—存放操作数或运算结果, OPTR—存放运算符号。 (2) 算法思想: 1)首先置操作数栈OPND为空栈,表达式的起始符#为运算符栈OPTR的栈底元素; 2)依次读入表达式中的每个字符, 若运算符是‘#’或栈顶是‘#’,结束计算,返回OPND栈顶值。 if(是操作数) → 则PUSH( OPND,操作数); if(是运算符) → 则与OPTR栈顶元素进行比较,按优先级进行操作; 3)直到整个表达式求值完毕(当前读入的字符和OPTR栈的栈顶元素均为# )
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 )
例2.迷宫求解 通常用的是“穷举求解”的方法
迷宫路径算法的基本思想 若当前位置“可通”,则纳入路径,继续前进; 若当前位置“不可通”,则后退,换方向继续探索; 若四周“均无通路”,则将当前位置从路径中删除出去。
设定当前位置的初值为入口位置; do{ 若当前位置可通, 则{将当前位置插入栈顶; 若该位置是出口位置,则算法结束; 否则切换当前位置的东邻方块为新的当前位置; } 否则 {若栈不空且栈顶位置尚有其他方向未被探索, 则设定新的当前位置为: 沿顺时针方向旋转找到的栈顶位置的下 一相邻块; 若栈不空但栈顶位置的四周均不可通, 则{ 删去栈顶位置;// 从路径中删去该通道块 若栈不空,则重新测试新的栈顶位置, 直至找到一个可通的 相邻块或出栈至栈空; } }while (栈不空);
大家来思考 讨论 ??? 问题: 求n!的递归方法的思路是:
大家来思考 ??? 相应的C语言函数是: float fact(int n) {float s; if (n= =0||n= =1) s=1; else s=n*fact(n-1); return (s); } 在该函数中可以理解为求n!用fact(n)来表示,则求(n-1)!就用fact(n-1)来表示。 若求5!,则有 main() { printf(“5!=%f\n”,fact(5)); } 执行过程如下:
主函数 mani() printf(“fact(5)”) 第一层调用 n=5 s=5*fact(4) 第二层调用 n=4 s=4*fact(3) 第三层调用 n=3 S=3*fact(2) 第四层调用 n=2 S=2*fact(1) 第五层调用 n=1 S=1 fact(1)=1 fact(2)=2 fact(3)=6 fact(4)=24 fact(5)=120 输出s=120.00 ,fact(5)为主函数调用,其它则为在fact函数内的调用。每一次递归调用并未立即得到结果,而是进一步向深度递归调用,直到n=1或n=0时,函数fact才有结果为1,然后再一一返回计算,最终得到结果。 图 递归调用过程示意图
练习一下吧 1. 一个栈的输入序列为123…n,若输出序列的第一个元素是n,输出第i(1<=i<=n)个元素是( )。 A. 不确定 B. n-i+1 C. i D. n-i 2. 若一个栈的输入序列为1,2,3,…,n,输出序列的第一个元素是i,则第j个输出元素是( )。 A. i-j-1 B. i-j C. j-i+1 D. 不确定的 3. 若已知一个栈的入栈序列是1,2,3,…,n,其输出序列为p1,p2,p3,…,pN,若pN是n,则pi是( )。 A. i B. n-i C. n-i+1 D. 不确定 4. 有六个元素6,5,4,3,2,1 的顺序进栈,问下列哪一个不是合法的出栈序列?( )A. 5 4 3 6 1 2 B. 4 5 3 1 2 6 C. 3 4 6 5 2 1 D. 2 3 4 1 5 6
小结 • 栈:是一种只允许在一端进行插入和删除的线性表,它是一种操作受限的线性表。在表中只允许进行插入和删除的一端称为栈顶(top),另一端称为栈底(bottom)。栈顶元素总是最后入栈的,因而是最先出栈;栈底元素总是最先入栈的,因而也是最后出栈。因此,栈也被称为“后进先出”的线性表。 • 栈的顺序存储结构:利用一组地址连续的存储单元依次存放自栈底到栈顶的各个数据元素,称为栈的顺序存储结构。 • 栈的链式存储结构:栈的链式存储结构就是用一组任意的存储单元(可以是不连续的)存储栈中的数据元素,这种结构的栈简称为链栈。在一个链栈中,栈底就是链表的最后一个结点,而栈顶总是链表的第一个结点。
3.4 队列 • 3.4.1 抽象数据类型队列的定义 • 3.4.2 链队列 • 3.4.3 循环队列
出队列 入队列 a1 ,a2, a3,…,an 3.4.1 队列 定义:只允许在表的一端进行插入,而在另一端删除元素的线性表。 在队列中,允许插入的一端叫队尾(rear) ,允许删除的一端称为对头(front)。 特点:先进先出 (FIFO) 队 头 队 尾
顺序队列:用连续的空间区域存放一个队列,设置两个指针分别指向队头合队尾。顺序队列:用连续的空间区域存放一个队列,设置两个指针分别指向队头合队尾。 • 循环队列:为解决顺序队列中的溢出现象而设置的头尾连接的队列。 • 链式队列:每个元素定义成一个结点,含两个域:其中数据域:存放结点数据,
data next front data next front rear rear 3.4.2 链队列 • 链队列中,有两个分别指示队头和队尾的指针。 • 链式队列在进队时无队满问题,但有队空问题。
^ front NULL front 空队列 rear rear ^ front 元素x入队 x ^ rear front 元素y入队 x ^ y ^ rear front 元素x出队 x ^ y ^ rear
3.4.3 循环队列: • 1.顺序队列: • 下图显示顺序表入队出队的过程: • 从示意图中可以看到,随着入队、出队操作的进行,整个队列会整体向后移动,这样就出现了最后图中的现象:队尾指针虽然已经移到了最后,而队列却未真满的“假溢出”现象,使得队列的空间没有得到有效的利用。
A B C D rear front 空队列 front rear A,B,C, D进队 C D E F G C D front rear E,F,G进队 front rear A,B出队 C D E F G rear front H进队,溢出 图 rear=9
6 7 Maxsize-1 5 0 4 1 3 2 front 循环队列 2.循环队列 问题:我们如何来解决“假溢出”现象? • 为了解决上述队列的“假溢出”现象,要做移动操作,当移动数据较多时将会影响队列的操作速度。一个更有效的方法是将队列的数据区Q[0 ..MAXLEN-1]看成是首尾相连的环,即将表示队首的元素Q[0]与表示队尾的元素Q[MAXLEN–1]连接起来,形成一个环形表,这就成了循环队列,如图所示。
大家来思考 问题 ??? 回答: 不可以,常用以下方法: 问题: 循环队列中,根据等式front= =rear可以判别队满和队空?
仅根据等式front= =rear 无法有效判别是“队满”还是“队空”。 • 两种常用的方法: • (1)规定:当front= =rear,表示循环队列为空; • 当front= =(rear+1)% MAXLEN,表示循环队列为满。 • (2)在定义结构体时,附设一个存储循环队列中元素个数的变量n,当n= =0时表示队空; • 当n= =MAXLEN时为队满。
循环队列条件: 队头、队尾指针加1,可用取模(余数)运算实现。 队头指针进1: front = (front+1) %maxsize; 队尾指针进1: rear = (rear+1) % maxsize; 队列初始化:front = rear = 0; 队空条件:front == rear; 队满条件:(rear+1) % maxsize == front;
练习一下吧 • 1. 循环队列存储在数组A[0..m]中,则入队时的操作( )。A. rear=rear+1 B. rear=(rear+1) mod (m-1) • C. rear=(rear+1) mod m D. rear=(rear+1)mod(m+1) • 2. 若用一个大小为6的数组来实现循环队列,且当前rear和front的值分别为0和3,当从队列中删除一个元素,再加入两个元素后,rear和front的值分别为多少? • A.1和 5 B. 2和4 C. 4和2 D. 5和1 • 3.栈和队列的共同点是( )。 • A. 都是先进先出 B. 都是先进后出 • C. 只允许在端点处插入和删除元素 D. 没有共同点
小结 • 队列:队列(queue)是一种只允许在一端进行插入,而在另一端进行删除的线性表,它是一种操作受限的线性表。在表中只允许进行插入的一端称为队尾(rear),只允许进行删除的一端称为队头(front)。队头元素总是最先进队列的,也总是最先出队列;队尾元素总是最后进队列,因而也是最后出队列。因此,队列也被称为“先进先出”表。 • 队列的顺序存储结构:利用一组地址连续的存储单元依次存放队列中的数据元素,称为队列的顺序存储结构。 • 队列的链式存储结构:队列的链式存储结构就是用一组任意的存储单元(可以是不连续的)存储队列中的数据元素,这种结构的队列称为链队列。在一个链队列中需设定两个指针(头指针和尾指针)分别指向队列的头和尾。
作 业 • 何谓队列的上溢现象?解决它有哪些方法,且分别简述其工作原理。 • 画出对算术表达式A-B*C/D-E求值时操作数栈和运算符栈的变化过程。 • 完成课本49页,括号匹配算法。