slide1 n.
Download
Skip this Video
Loading SlideShow in 5 Seconds..
第三章 栈和队列 PowerPoint Presentation
Download Presentation
第三章 栈和队列

Loading in 2 Seconds...

play fullscreen
1 / 98

第三章 栈和队列 - PowerPoint PPT Presentation


  • 237 Views
  • Uploaded on

第三章 栈和队列. 教学内容 1 、栈和队列的定义及特点; 2 、栈的顺序存储表示和链接存储表示; 3 、队列的顺序存储表示和链接存储表示; 4 、栈和队列的应用举例。. 第三章 栈和队列. 教学要求 1 、掌握栈和队列的定义、特性,并能正确应用它们解决实际问题; 2 、熟练掌握栈的顺序表示、链表表示以及相应操作的实现。特别注意栈空和栈满的条件; 3 、熟练掌握队列的顺序表示、链表表示以及相应操作的实现。特别是循环队列中队头与队尾指针的变化情况。. 第三章 栈和队列. 3.1 栈 3.2 栈的应用举例 3.3 栈与递归的实现

loader
I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.
capcha
Download Presentation

PowerPoint Slideshow about '第三章 栈和队列' - tanith


An Image/Link below is provided (as is) to download presentation

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -
Presentation Transcript
slide1
第三章 栈和队列
  • 教学内容

1、栈和队列的定义及特点;

2、栈的顺序存储表示和链接存储表示;

3、队列的顺序存储表示和链接存储表示;

4、栈和队列的应用举例。

slide2
第三章 栈和队列
  • 教学要求

1、掌握栈和队列的定义、特性,并能正确应用它们解决实际问题;

2、熟练掌握栈的顺序表示、链表表示以及相应操作的实现。特别注意栈空和栈满的条件;

3、熟练掌握队列的顺序表示、链表表示以及相应操作的实现。特别是循环队列中队头与队尾指针的变化情况。

slide3
第三章 栈和队列

3.1 栈

3.2 栈的应用举例

3.3 栈与递归的实现

3.4 队列

slide4
一、栈的定义及特点

1、栈的定义:

限定仅在表尾进行插入或删除操作的线性表,因此,对栈来说,表尾端有其特殊含义,表尾—栈顶(top),表头—栈底(bottom),不含元素的空表称空栈。

进栈

出栈

...

栈顶

an

……...

栈s=(a1,a2,……,an)

a2

栈底

a1

3.1 抽象数据类型栈的定义

允许插入(入栈)、删除(出栈)的一端叫栈顶

不允许插入、删除的一端叫栈底

slide5

2、栈的特点

最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。

即,栈是一种后进先出(Last In First Out)的线性表,简称为LIFO表。(先进后出(FILO))

3、栈的例子及用途

例一:放在地板上的一叠书,只能从顶上面拿走一本,若再加入一本,也只能放在顶上。

例二:子弹的压入、发射。

例三:盘子的刷洗及使用。

例四:走入死胡同的人。

例五:函数的嵌套调用值的返回。

slide6

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 (不含任何元素)。

slide7

DestroyStack(&S)

初始条件:栈s已存在。

操作结果:栈s被销毁。

ClearStack(&S)

初始条件:栈s已存在。

操作结果:将s清为空栈。

StackEmpty(S) //判栈空

初始条件:栈s已存在。

操作结果:若栈S为空栈,则返回TRUE,

否则FALSE。

slide8

StackLength(S)

初始条件:栈s已存在。

操作结果:返回s的元素个数,即栈的长度。

GetTop(S,&e) //取栈顶元素

初始条件:栈s已存在且非空。

操作结果:用e返回s的栈顶元素。

Push(&S,e) //进栈

初始条件:栈s已存在。

操作结果:插入元素e为新的栈顶元素。

“入栈”、 “插入”、 “压入”

slide9

Pop(&S,&e) //出栈

初始条件:栈s已存在且非空。

操作结果:删除s的栈顶元素,并用e返回其值。

也称为“退栈”、 “删除”、 “弹出”。

StackTraverse(S,visit())

初始条件:栈s已存在且非空。

操作结果:从栈底到栈顶依次对s的每个数据元素调用函数visit()。一旦visit()失败,则操作失效。

} ADT Stack

slide10

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的输出呢?例2:一个栈的输入序列是12345,若在入栈的过程中允许出栈,则栈的输出序列43512可能实现吗?12345的输出呢?

答:

43512不可能实现,主要是其中的12顺序不能实现;

12345的输出可以实现.

例3:如果一个栈的输入序列为123456,能否得到435612和135426的出栈序列?

435612中到了12顺序不能实现;

135426可以实现。

答:

slide12

例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。

slide13

例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。

slide14

a1 a2 a3 a4 …

base

top

二、栈的顺序表示和实现(顺序栈)

利用一组地址连续的存贮单元依次自栈底到栈顶存放栈的数据元素。栈底元素是最先进入的,实际上是线性表的第一个元素。

//动态分配空间(表示方法1)

typedef struct

{

SElemType *Base;// 栈底指针

int top;// 栈顶指针

int StackSize;//当前已分配的存储空间,以元素为单位。

}SqStack;

注意:栈空和栈满条件!

top==-1 和 top>=S.StackSize-1

slide15

// 动态分配顺序栈空间(表示方法2)

#define STACK_INIT_SIZE 100

#define STACKINCREMENT 10

typedef struct

{SElemType *Base; // 栈底指针

SElemType *Top;// 栈顶指针

int StackSize; //当前已分配的存储空间,以元素为单位

}SqStack;

slide16

a1 a2 a3 a4 …

base

top

静态分配顺序栈空间表示

#define MaxSize 100

typedef struct

{

ElemType base[MaxSize];

// base为栈底指针

int top;// top栈顶指针

}SqStack;//顺序栈类型定义

注意:栈空和栈满条件!

top==-1 和 top>=MaxSize-1

slide17

A

top

top

base

base

空栈

A进栈

  • s.top=s.base 表示空栈;
  • s.base=NULL表示栈不存在;
  • 当插入新的栈顶元素时,先在栈顶指针位置放入新元素,然后栈顶指针s.top++;
  • 删除栈顶元素时,栈顶指针s.top- -,然后再删除;
  • 当s.top-s.base>=stacksize时,栈满,溢出
slide18

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;

slide19
顺序栈基本操作算法描述

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

slide20
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

slide21
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;

}

slide22
*s.top = e;

s.top++;

return OK;

}// Push

slide23
4、出栈操作

Status Pop( SqStack &S, SElemType &e )

{// 出栈

if( s.top == s.base ) // 空吗?

{

return ERROR;

}

s.top --;

e = *s.top;

return OK;

}// Pop

slide24
栈的共享存储单元

有时,一个程序设计中,需要使用多个同一类型的栈,这时候,可能会产生一个栈空间过小,容量发生溢出,而另一个栈空间过大,造成大量存储单元浪费的现象。 为了充分利用各个栈的存储空间,这时可以采用多个栈共享存储单元,即给多个栈分配一个足够大的存储空间,让多个栈实现存储空间优势互补。

slide25

an an-1 a1

三、栈的链式存储表示(链栈)

栈的链式存储结构称为链栈,它的运算是受限的单链表,插入和删除操作仅限制在表头位置上进行。

slide26
链栈特点:
  • 链式栈无栈满问题,空间可扩充
  • 链式栈的栈顶在链头
slide27
栈的链式存储结构定义

typedef struct SNode

{ ElemType data;

struct SNode *next;

}SNode,*LinkStack;

slide28
链栈的进栈算法

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;

}

slide29
链栈的出栈算法

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;

}

slide30
作业

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、假设表达式中允许包含三种括号:圆括号、方括号和大括号。编写一个算法,判定表达式中的括号是否正确配对。

slide31
第三章 栈和队列

3.1 栈

3.2 栈的应用举例

3.3 栈与递归的实现

3.4 队列

slide32

3.2 栈的应用举例

一、 数制转换

二、 括号匹配的检验

三、 行编辑程序问题

四、 表达式求值

五、 迷宫求解

六、 实现递归

slide33
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);//输出

}

}

slide34
一、数制转换

例如 (1348)10=(2504)8,

其运算过程如下:

n(被除数) n div 8(商) n mod 8(余数)

1348 168 4

168 21 0

21 2 5

2 0 2

0

计算顺序

输出顺序

slide35
算法分析:

由于十进制转换为其他进制数的规则,可知,求得的余数的顺序为由低位到高位,而输出则是由高位到低位。

因此,这正好利用栈的特点,先将求得的余数依次入栈,输出时,再将栈中的数据出栈。

slide36
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

slide37
二、括号匹配的检验

在表达式中

([]())或[([ ][ ])]

等为正确的格式,

([]( ) 或(()))或 [( ])均为不正确的格式。

则 检验括号是否匹配的方法可用“期待的急迫程度”这个概念来描述。

slide38
算法的设计思想:

读入表达式

1)凡出现左括弧,则进栈;

2)凡出现右括弧,首先检查栈是否空,

若栈空,则表明该“右括弧”多余,

否则和栈顶元素比较,

若相匹配,则“左括弧出栈” ,

否则表明不匹配。

3)表达式检验结束时,

若栈空,则表明表达式中匹配正确,

否则表明“左括弧”有余。

slide39
三、行编辑程序问题

并不恰当!

“每接受一个字符即存入存储器” ?

在用户输入一行的过程中,允许用户输入出差错,并在发现有误时可以及时更正。

合理的作法是:

设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区,并假设“#”为退格符,“@”为退行符。

slide40
算法的设计思想:

可设这个输入缓冲区为一个栈结构,每当从终端接受了一个字符之后先作如下判断:

1、既不是退格也不是退行符,则将该字符压入栈顶;

2、如果是一个退格符,则从栈顶删去一个字符;

3、如果是一个退行符,则将字符栈清为空栈 。

slide41
四、表达式求值

表达式求值是程序设计语言编译的一个最基本的问题。它的实现是栈应用的又一典型例子。 仅以算术表达式为例。

1、算术表达式的组成:

将表达式视为由操作数、运算符、界限符组成。

操作数:常数、变量或符号常量。

算符:运算符、界限符

slide42

2、算术表达式的形式

  • 中缀(infix)表达式——表达式的运算符在操作数的中间。<操作数><操作符><操作数>

例:A*B 例:5+9*7

  • 后缀(postfix)算术表达式(逆波兰式)——将运算符置两个操作数后面的算术表达式。

<操作数> <操作数><操作符>

例:AB* 例:5 9 7*+

  • 前缀(prefix)表达式(波兰式),与后缀表达式相反,把运算符放在两个运算数的前面。

<操作符><操作数> <操作数>

例:*AB 例:+5*9 7

slide43
3、介绍中缀算术表达式的求值

例如:3*(7 – 2 )

(1)算术四则运算的规则:

a. 从左算到右

b. 先乘除,后加减

c. 先括号内,后括号外

由此,此表达式的计算顺序为:

3*(7 – 2 )= 3 * 5 = 15

slide44

(2)根据上述三条运算规则,在运算的每一步中,对任意相继出现的算符1和2,都要比较优先权关系。(2)根据上述三条运算规则,在运算的每一步中,对任意相继出现的算符1和2,都要比较优先权关系。

一般任意两个相继出现的两个算符 1和 2之间的优先关系至多有下面三种之一:

 1< 22的优先权高于 1

 1= 2二者优先权相等

 1> 2 2的优先权低于 1

算符优先法所依据的算符间的优先关系见教材。

slide45

θ1<θ2,表明不能进行θ1 的运算,θ2 入栈,读 下一字符。

θ1>θ2,表明可以进行θ1 的运算,θ1退栈,运算,运算结果入栈。

θ1=θ2,脱括号,读下一字符或表达式结束。

slide46

例如:3*(7 – 2 )

  • 一般作为相同运算符,先出现的比后出现的优先级高;
  • 先出现的运算符优先级低于“(”,高于“)”;
  • 后出现的运算符优先级高于“(”,低于“)”;优先权相等的仅有“(”和“)”、“#”。
  • #:作为表达式结束符,通常在表达式之前加一“#”使之成对,当出现“#”=“#”时,表明表达式求值结束,“#”的优先级最低。
slide47

(3)算法思想:

  • 设定两栈:操作符栈 OPTR ,操作数栈 OPND
  • 栈初始化:设操作数栈 OPND 为空;操作符栈 OPTR 的栈底元素为表达式起始符 ‘#’;
  • 依次读入字符:是操作数则入OPND栈,是操作符则要判断:

if 操作符 < 栈顶元素,则退栈、计算,结果压入OPND栈;

操作符 = 栈顶元素且不为‘#’,脱括号(弹出左括号);

操作符 > 栈顶元素,压入OPTR栈。

slide48

定义运算符栈S

和操作数栈O

扫描基本符号

是否操作数?

Y

N

操作数

入栈

栈顶运算符低

于当前运算符?

N

Y

取出S栈顶运算符和

O栈顶的两个操作数

运算,结果入栈O

运算符

入栈

Y

N

栈S为空?

Y

结束

slide49

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)

slide50

4、计算后缀表达式

(1)为什么要把中缀表示法变为后缀表示法?

计算机计算表达式是机械执行,只能从左至右扫描。计算中缀表达式比较困难。

后缀表达式的求值过程是自左至右运算符出现的次序和真正的计算次序是完全一样的。

顺序扫描表达式的每一项,根据它的类型做如下相应操作:

若该项是操作数,则将其压栈;

若该项是操作符<op>,则连续从栈中退出两个操作数Y和X,形成运算指令X<op>Y,并将计算结果重新压栈。

表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。

人们仍然使用中缀表达式 ,而让机器把它们转换成后缀表达式。

slide51
(2)后缀表达式求值实例:

A=B*C + D*(E-F)

ABC*DEF-*+=

ABC*—做 B*C=T1

AT1DEF- –—做E-F=T2

AT1DT2*—做D*T2=T3

AT1T3+—做T1+T3=T4

AT4=—做A=T4

F

T2

E

C

D

T3

B

T1

T4

A

A=T4

slide52

typedef char elemtype ;

double calcul_exp(char *A) /*本函数返回由后缀表达式A表示的表达式运算结果*/

{ Seq_Starck s ;

ch=*A++ ; Init_SeqStack(s) ;

while ( ch != ’#’ )

{ if (ch!=运算符) Push_SeqStack (s , ch) ;

else { Pop_SeqStack (s , &a) ;

Pop_SeqStack (s , &b) ; /*取出两个运算量*/

switch (ch).

{ case ch= =’+’: c=b+a ; break ;

case ch= =’-’: c=b-a ; break ;

case ch= =’*’: c=b*a ; break ;

case ch= =’/ ’: c=b/a ; break ;

case ch= =’%’: c=b%a ; break ; }

Push_SeqStack (s, c) ; }

ch=*A++ ;

}

Pop _SeqStack ( s , result ) ;

return result ;

}

slide53
(3)如何将一个中缀表达式转换为一个后缀表达式?(3)如何将一个中缀表达式转换为一个后缀表达式?

假设用exp字符数组存储中缀算术表达式,其对应后缀表达式存放在字符数组postexp中。在将中缀表达式转换为后缀表达式的过程中要借助于一个栈,用于存放运算符。

  • 初始化运算符栈op,,将‘=’进栈作为栈底元素(它是为了算法设计方便另增的,其优先级最低)。
  • 从exp中读取字符ch,若ch是运算数,则将后续的所有数字依次存放到postexp中,并以字符‘#’标志数值串结束。若ch是运算符,则将其和op栈顶运算符的优先级进行比较,若大于,则将ch进栈并取exp下一个字符,否则,退栈运算符并存放到postexp中。重复这一过程直到exp扫描完毕,此时若栈op不空,将‘=’之前的所有运算符退栈并放于postexp中。
slide57
课堂练习
  • 请把下列中缀表达式分别转换为前缀表达式(波兰式)和后缀表达式(逆波兰式)。

7*(55-13)/(35+14)

slide58

五、迷宫求解

求迷宫问题就是求出从入口到出口的路径。在求解时,通常用的是“穷举求解”的方法,即从入口出发,顺某一方向向前试探,若能走通,则继续往前走;否则沿原路退回,换一个方向再继续试探,直至所有可能的通路都试探完为止。为了保证在任何位置上都能沿原路退回(称为回溯),需要用一个后进先出的栈来保存从入口到当前位置的路径。

slide59
用如下图所示的方块图表示迷宫。对于图中的每个方块,用空白表示通道,用阴影表示墙。所求路径必须是简单路径,即在求得的路径上不能重复出现同一通道块。
slide60
第三章 栈和队列

3.1 栈

3.2 栈的应用举例

3.3 栈与递归的实现

3.4 队列

slide61
3.3 栈与递归的实现

一、递归的定义

若一个对象部分地包含它自己, 或用它自己给自己定义, 则称这个对象是递归的;若一个函数直接地或间接地调用自己, 则称这个函数是递归函数。

递归满足两个条件:1. 不断调用函数本身,也就是递归函数(递归体)。2. 调用是有限的,也就是有递归出口。

即:递归出口确定递归到何时结束,

递归体确定递归求解时的递推关系。

以下三种情况常常用到递归方法。

  • 定义是递归的
  • 数据结构是递归的
  • 问题的解法是递归的
slide62
1、定义是递归的

例1:阶乘函数

求解阶乘函数的递归算法

long Fact ( long n )

{

if ( n == 0 ) return 1;

else return n * Fact (n-1);

}

slide63
2、数据结构是递归的

typedef struct LNode

{

ElemType data;

struct LNode*next;

} LNode,*LinkList;

例2:单链表结构

f

f

  • 一个结点,它的指针域为NULL,是一个单链表;
  • 一个结点,它的指针域指向单链表,仍是一个单链表。
slide64
遍历单链表的递归算法(逆序输出):

void OutLinkList(LinkList L)

{

if(L)

{

OutLinkList(L->next);

printf(L->data);

}

}

slide65
3、问题的解法是递归的

有些问题的解法是递归的,即规模为n的问题求解,可以化为规模为n-1的问题求解,而二者的求解方法是相同,特别地当问题规模达到一定程度时(如n=1)可以得到一个确定解。

典型的有汉诺塔(Tower of Hanoi)问题求解。

slide66
二、递归函数执行过程

在高级语言中,调用函数和被调用函数之间的链接及信息交换需要通过栈来进行。此栈不需用户自己管理,而是由系统来管理工作栈的。

n=4

n=3

n=2

n=1

n=0

例如,主函数中调用Fact(4)

long Fact ( long n )

{

if ( n == 0 ) return 1;

else return n * Fact (n-1);

}

3*Fact(2)

4*Fact(3)

2*Fact(1)

1*Fact(0)

return 24

return 6

return 2

return 1

return 1

slide67
由于递归函数结构清晰,程序易读,而且它的正确性容易得到证明,因此,利用允许递归调用的语言(如C语言)进行程序设计时,给用户编制程序和调式程序带来很大方便。由于递归函数结构清晰,程序易读,而且它的正确性容易得到证明,因此,利用允许递归调用的语言(如C语言)进行程序设计时,给用户编制程序和调式程序带来很大方便。

但递归过程效率低,重复计算多,有时为提高效率,需要把递归函数转化为非递归函数。把递归算法转化为非递归算法有如下三种基本方法:

(1)通过分析,跳过分解过程,直接用循环结构的算法实现求值过程。例:求n!.

(2)利用栈保存参数,由于栈的后进先出特性吻合递归算法的执行过程,因而可以用非递归算法替代递归算法.

(3)自己用栈模拟系统的运行时栈,通过分析只保存必须保存的信息,从而用非递归算法替代递归算法。

slide68
三、递归消除

求解阶乘函数的非递归算法:

long Fact ( long n )

{

InitStack(s);

t=1;

while(n)

{

Push(s,n);

n--;

}

while(!StackEmpty(s))

{

Pop(s,e);

t=t*e;

}

return t;

}

slide69
本节小结
  • 栈的逻辑结构及其顺序和链式两种存储结构
  • 栈的重点在于栈的应用
slide70
第三章 栈和队列

3.1 栈

3.2 栈的应用举例

3.3 栈与递归的实现

3.4 队列

slide71
3.4 队列

一、 队列的定义

队列(Queue)也是一种运算受限的线性表。它只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。

当队列中没有元素时称为空队列。在空队列中依次加入元素a0,a1,…an-1之后,a0是队头元素,an-1是队尾元素。显然退出队列的次序也只能是a0,a1,…an-1。先进入队列的成员总是先离开队列。因此队列亦称作先进先出(First In First Out)的线性表,简称FIFO表。

slide72
3.4 队列

二、 队列的抽象数据定义

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端为队列尾。

基本操作:

1.InitQueue(&Q)

将队列Q设置成一个空队列。

2.DestroyQueue (&Q)

初始条件:队列Q已存在。操作结果:队列Q被销毁,不再存在

slide73
3.EnQueue(&Q,X)

将元素X插入到队尾中,也称“进队” ,“插入”。

4.DeQueue(&Q,&e)

将队列Q的队头元素删除,并用e返回其值,也称“退队”、“删除”。

5.GetHead(Q,&e)

得到队列Q的队头元素之值,并用e返回其值。

6. QueueEmpty(Q)

判断队列Q是否为空,若为空返回1,否则返回0。

slide74
7. ClearQueue (&Q)

初始条件:队列Q已存在。

操作结果:将Q清为空队列。

8. QueueLength (Q)

初始条件:队列Q已存在

操作结果:返回Q的元素个数,即队列的长度。

9.QueueTraverse(Q,visit())

初始条件:Q已存在且非空。

操作结果:从队头到队尾,依次对Q的每个数据元素调用函数visit() 。一旦visit失败,则操作失败。

}ADT Queue

slide75

插入 插入

a1 a2 a3… an

删除 删除

除了栈和队列之外,还有一种限定性数据结构是双端队列(Deque)。双端队列是限定插入和删除操作在表的两端进行的线性表。在实际使用中,还可以有输出受限的双端队列(即一个端点允许插入和删除,另一个端点只允许插入的双端队列)和输入受限的双端队列(即一个端点允许插入和删除,另一个端点只允许删除的双端队列)。而如果限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈了。

(a) 双端队列 (b) 铁道转轨网

图3.9 双端队列示意图

slide76

data next

Q.front

队头

Q.rear

队尾

三、队列的链式表示和实现

用链表表示的队列简称为链队列,需要两个分别指示队头和队尾的指针(分别称为头指针和尾指针)才能唯一确定。

给链队列添加一个头结点,并令头指针指向头结点。

空的链队列的判决条件:头指针和尾指针均指向头结点。

slide77

Q.front

Q.rear

Q.front

Q.rear

x

y

Q.front

Q.rear

Q.front

Q.rear

x

x

y

链队列的操作即为单链表的插入和删除操作的特殊情况,只是尚需修改尾指针或头指针,图3.11(a)~(d)展示了这两种操作进行时指针变化的情况。

(a)空队列 (c)元素y入队列

(b)元素x入队列 (d)元素x出队列

图3.11 队列运算指针变化情况

slide78
//====ADT Queue的表示与实现===

//——单链队列——队列的链式存储结构

typedef struct QNode{

QElemType data;

struct QNode *next;

}QNode,*QueuePtr;

typedefstruct {

QueuePtr front; //队头指针

QueuePtr rear;//队尾指针

} LinkQueue;

slide79

链队列基本运算实现

1、初始化链队列

Status InitQueue ( LinkQueue &Q)

{ //构造一个空队列

Q.front=Q.rear=(QueuePtr)malloc (sizeof(QNode));

if(!Q.front) exit (OVERFLOW);

Q.front—>next=NULL;

return OK;

}

slide80

2、入队

Status EnQueue(LinkQueue &Q, QElemType e)

{ //插入元素e为Q的新的队尾元素

p= QueuePtr)malloc(sizeof(QNode));

if(!p) exit(OVERFLOW);

p—>data = e; p—>next = NULL;

Q.rear—>next = p;

Q.rear=p;

return OK;

}

slide81

3、出队

Status DeQueue (LinkQueue &Q, QElemType &e)

{//若队列不空,则删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR;

if (Q.front == Q.rear) 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;

}

slide82
注意:删除队列头元素的特殊情况。一般情况下,删除队列头元素时仅需修改头结点中的指针,但当队列中最后一个元素被删后,队列尾指针也丢失了,因此需对队尾指针重新赋值(指向头结点)。注意:删除队列头元素的特殊情况。一般情况下,删除队列头元素时仅需修改头结点中的指针,但当队列中最后一个元素被删后,队列尾指针也丢失了,因此需对队尾指针重新赋值(指向头结点)。
3 4 3
3.4.3 循环队列——队列的顺序表示和实现

和顺序栈相类似,在队列Q的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队列头到队列尾的元素之外,尚需附设两个指针front和rear分别指示队列头元素和队列尾元素的位置。为了在C语言中描述方便起见,在此我们约定:

初始化建空队列时,令front = rear = 0,

插入新的队列尾元素时,“尾指针增1”;

删除队列头元素时,“头指针增1”。

因此,在非空队列中,头指针始终指向队列头元素,而尾指针始终指向队列尾元素的下一个位置。

slide84

(a)空队列;

(b)J1、J2和J3,相继入队列;

(b)J1和J2相继被删除;

(d)J4、J5和J6相继插入队列之后J3、 J4被删除。

(a) (b) ( c ) (d)

图3.12 头、尾指针和队列中元素之间的关系

Q.rear

J6

Q.front

Q.rear

Q.rear

J5

Q.front

J3

J3

Q.rear

Q.front

J2

Q.front

J1

slide85

假设当前为队列分配的最大空间为6,则当队列处于图3.12(d)的状态时不可再继续插入新的队尾元素,否则数组越界而遭致程序代码被破坏。然而此时又不宜进行存储再分配扩大数组空间,因为队列的实际可用空间并未占满。这种现象为“假溢出”,这是由于“队尾入队头出”这种受限制的操作所造成。假设当前为队列分配的最大空间为6,则当队列处于图3.12(d)的状态时不可再继续插入新的队尾元素,否则数组越界而遭致程序代码被破坏。然而此时又不宜进行存储再分配扩大数组空间,因为队列的实际可用空间并未占满。这种现象为“假溢出”,这是由于“队尾入队头出”这种受限制的操作所造成。

一个较巧妙的办法是将顺序队列臆造为一个环状的空间,如图3.13所示,称之为循环队列。

maxsize-1

0

1

Q.rear

Q.front

空闲空间

图 3.13

slide86

Q.rear

Q.front

Q.front

Q.rear

Q.front

Q.rear

Q.front

J6

J6

J5

J5

Q.rear

J4

J9

J3

J8

J7

J7

(a) J1、J2 、J3和J4相继插入队列之后J1、 J2被删除。

(b)J3和J4相继被删除,队列成为空队列;

(c)J5、J6和J7,相继入队列;;

(d)J8和J9相继插入队列后,队满。

(a) (b) ( c ) (d)

图3.12 头、尾指针和队列中元素之间的关系

slide87
“队满”和“队空”的解决方法

方法一:

附设一个存储队中元素个数的变量如num,当num==0时队空,当num==MAXSIZE时为队满。方法二:

少用一个元素空间,把图(d)所示的情况就视为队满,此时的状态是队尾指针加1就会从后面赶上队头指针,这种情况下队满的条件是:(rear+1) % MAXSIZE ==front,也能和空队区别开。方法三:

另设一个标志位以区别队列是“空”还是“满”。

slide88

队列存放数组被当作首尾相接的表处理。

  • 队头、队尾指针加1时从maxSize -1直接进到0,可用语言的取模(余数)运算实现。
  • 队头指针进1: Q.front = (Q.front+1) % maxSize;
  • 队尾指针进1: Q.rear = (Q.rear+1) % maxSize;
  • 队列初始化:Q.front = Q.rear = 0;
  • 队空条件:Q.front== Q.rear;
  • 队满条件:(Q.rear+1) % maxSize == Q.front
slide89

从上述分析可见,如果用户的应用程序中设有循环队列,则必须为它设定一个最大队列长度;若用户无法估计所用队列的最大长度,则宜采用链队列。

循环队列类型的模块说明如下:

//—————循环队列——队列的顺序存储结构—————

#define MAXQSIZE 100 //最大队列长度

typedef struct {

QElemType *base;//初始化的动态分配存储空间

int front;//头指针,若队列不空,指向队列头元素

int rear;//尾指针,若队列不空,指向队列尾元素的下一个位置

}SqQueue ;

slide90

循环队列的基本运算实现

1、初始化循环队列

Status InitQueue (SqQueue &Q)

{ //构造一个空队列Q

Q.base=(QElemType)malloc(MAXQSIZE*sizeof(QElemType));

if(!Q.base)exit(OVERFLOW); //存储分配失败

Q.front = Q.rear = 0;

return OK;

}

slide91

2、求循环队列长度

int QueueLength (SqQueue Q)

{ //返回Q的元素个数,即队列的长度

return(Q.rear – Q.front + MAXQSIZE) % MAXQSIZE;

}

slide92

3、循环队列元素入队

Status EnQueue (SqQueue &Q,QElemType e)

{ //插入元素e为Q的新的队尾元素

if((Q.rear+1)%MAXQSIZE==Q.front)

return ERROR; //队列满

Q.base[Q.rear] = e;

Q.rear = (Q.rear+1) % MAXQSIZE;

return OK;

}

slide93

4、循环队列元素出队

Status DeQueue (SqQueue &Q,QElemType &e)

{ //若队列不空,则删除Q的队头元素,用e返回其值,并返回OK

//否则返回ERROR

if (Q.Front = = Q.rear) return ERROR;

e = Q.base[ Q.front ];

Q.front = (Q.front+1)%MAXQSIZE;

return OK;

}

3 4 4
3.4.4 队列的应用
  • 求解报数问题
  • 迷宫求解问题
  • 离散事件模拟
  • 树的层次遍历问题
  • 图的广度遍历问题
slide95
求解报数问题

设有n个人站成一排,从左向右的编号分别为1—n,现从左往右报数“1,2,1,2,……”,数到“1”的人出列,数到“2”的立即站到队伍的最右端。报数过程反复进行,直到n个人都出列为止。要求给出他们出列顺序。

slide97
作业

1、设从键盘输入一整数序列a1,a2,……,an,试编程实现:当ai>0时,ai进队,当ai<0时,将队首元素出队,当ai=0时,表示输入结束。要求将队列处理成环形队列,入队和出队操作单独编写算法,并在异常情况时(如队满)能打印出错。

2、输入n(由用户输入)个10以内的数,每输入i(0≤i≤ 9)就把它插入到第i号队列中。最后把10个队中非空队列,按队列号从小到大的顺序串接成一条链,并输出该链的所有元素。

slide98

本章小结 1.1 栈和队列的定义、特性及其基本运算的定义1.2 栈和队列的顺序存储结构以及基本运算的实现1.3 栈和队列的链式存储结构以及基本运算的实现

线性表、栈与队列的异同点

相同点:逻辑结构相同,都是线性的;都可以用顺序存储或链表存储;栈和队列是两种特殊的线性表,即受限的线性表(只是对插入、删除运算加以限制)。

不同点:

① 运算规则不同,线性表为随机存取,而栈是只允许在一端进行插入和删除运算,因而是后进先出表LIFO;队列是只允许在一端进行插入、另一端进行删除运算,因而是先进先出表FIFO。

② 用途不同,线性表比较通用;堆栈用于函数调用、递归和简化设计等;队列用于离散事件模拟、多道作业处理和简化设计等。