清华大学数据结构讲义ch3.docx

上传人:b****6 文档编号:7577645 上传时间:2023-01-25 格式:DOCX 页数:29 大小:135.72KB
下载 相关 举报
清华大学数据结构讲义ch3.docx_第1页
第1页 / 共29页
清华大学数据结构讲义ch3.docx_第2页
第2页 / 共29页
清华大学数据结构讲义ch3.docx_第3页
第3页 / 共29页
清华大学数据结构讲义ch3.docx_第4页
第4页 / 共29页
清华大学数据结构讲义ch3.docx_第5页
第5页 / 共29页
点击查看更多>>
下载资源
资源描述

清华大学数据结构讲义ch3.docx

《清华大学数据结构讲义ch3.docx》由会员分享,可在线阅读,更多相关《清华大学数据结构讲义ch3.docx(29页珍藏版)》请在冰豆网上搜索。

清华大学数据结构讲义ch3.docx

清华大学数据结构讲义ch3

第三章栈和队列

栈和队列是在软件设计中常用的两种数据结构,它们的逻辑结构和线性表相同。

其特点在于运算受到了限制:

栈按“后进先出”的规则进行操作,队按“先进先出”的规则进行操作,故称运算受限制的线性表。

3.1栈

3.1.1栈的定义及基本运算

栈是限制在表的一端进行插入和删除的线性表。

允许插入、删除的这一端称为栈顶,另一个固定端称为栈底。

当表中没有元素时称为空栈。

如图3.1.1所示栈中有三个元素,进栈的顺序是a1、a2、a3,当需要出栈时其顺序为a3、a2、a1,所以栈又称为后进先出的线性表(LastInFirstOut),简称LIFO表。

a3

a2

a1

 

图3.1栈示意图

在日常生活中,有很多后进先出的例子,读者可以列举。

在程序设计中,常常需要栈这样的数据结构,使得与保存数据时相反顺序来使用这些数据,这时就需要用一个栈来实现。

对于栈,常做的基本运算有:

⑴栈初始化:

Init_Stack(s)

初始条件:

栈s不存在

操作结果:

构造了一个空栈。

⑵判栈空:

Empty_Stack(s)

初始条件:

栈s已存在

操作结果:

若s为空栈返回为1,否则返回为0。

⑶入栈:

Push_Stack(s,x)

初始条件:

栈s已存在

操作结果:

在栈s的顶部插入一个新元素x,x成为新的栈顶元素。

栈发生变化。

⑷出栈:

Pop_Stack(s)

初始条件:

栈s存在且非空

操作结果:

栈s的顶部元素从栈中删除,栈中少了一个元素。

栈发生变化。

⑸读栈顶元素:

Top_Stack(s)

初始条件:

栈s存在且非空

操作结果:

栈顶元素作为结果返回,栈不变化。

3.1.2栈的存储实现和运算实现

由于栈是运算受限的线性表,因此线性表的存储结构对栈也是适用的,只是操作不同而已。

1.顺序栈

利用顺序存储方式实现的栈称为顺序栈。

类似于顺序表的定义,栈中的数据元素用一个预设的足够长度的一维数组来实现:

datatypedata[MAXSIZE],栈底位置可以设置在数组的任一个端点,而栈顶是随着插入和删除而变化的,用一个inttop来作为栈顶的指针,指明当前栈顶的位置,同样将data和top封装在一个结构中,顺序栈的类型描述如下:

#defineMAXSIZE1024

typedefstruct

{datatypedata[MAXSIZE];

inttop;

}SeqStack

定义一个指向顺序栈的指针:

SeqStack*s;

通常0下标端设为栈底,这样空栈时栈顶指针top=-1;入栈时,栈顶指针加1,即s->top++;出栈时,栈顶指针减1,即s->top--。

栈操作的示意图如图3.2所示。

图(a)是空栈,图(c)是A、B、C、D、E5个元素依次入栈之后,图(d)是在图(c)之后E、D相继出栈,此时栈中还有3个元素,或许最近出栈的元素D、E仍然在原先的单元存储着,但top指针已经指向了新的栈顶,则元素D、E已不在栈中了,通过这个示意图要深刻理解栈顶指针的作用。

在上述存储结构上基本操作的实现如下:

top=-1top=0top=4top=2top=-1

(a)空栈(b)一个元素(c)5个元素(d)3个元素(e)空栈

图3.2栈顶指针top与栈中数据元素的关系

⑴置空栈:

首先建立栈空间,然后初始化栈顶指针。

SeqStack*Init_SeqStack()

{SeqStack*s;

s=malloc(sizeof(SeqStack));

s->top=-1;returns;

}

⑵判空栈

intEmpty_SeqStack(SeqStack*s)

{if(s->top==-1)return1;

elsereturn0;

}

⑶入栈

intPush_SeqStack(SeqStack*s,datatypex)

{if(s->top==MAXSIZE-1)return0;/*栈满不能入栈*/

else{s->top++;

s->data[s->top]=x;

return1;

}

}

⑷出栈

intPop_SeqStack(SeqStack*s,datatype*x)

{if(Empty_SeqStack(s))return0;/*栈空不能出栈*/

else{*x=s->data[s->top];

s->top--;return1;}/*栈顶元素存入*x,返回*/

}

⑸取栈顶元素

datatypeTop_SeqStack(SeqStack*s)

{if(Empty_SeqStack(s))return0;/*栈空*/

elsereturn(s->data[s->top]);

}

以下几点说明:

1.对于顺序栈,入栈时,首先判栈是否满了,栈满的条件为:

s->top==MAXSIZE-1,栈满时,不能入栈;否则出现空间溢出,引起错误,这种现象称为上溢。

2.出栈和读栈顶元素操作,先判栈是否为空,为空时不能操作,否则产生错误。

通常栈空时常作为一种控制转移的条件。

2.链栈

用链式存储结构实现的栈称为链栈。

通常链栈用单链表表示,因此其结点结构与单链表的结构相同,在此用LinkStack表示,即有:

typedefstructnode

{datatypedata;

structnode*next;

}StackNode,*LinkStack;

说明top为栈顶指针:

LinkStacktop;

因为栈中的主要运算是在栈顶插入、删除,显然在链表的头部做栈顶是最方便的,而且没有必要象单链表那样为了运算方便附加一个头结点。

通常将链栈表示成图3.3的形式。

链栈基本操作的实现如下:

⑴置空栈

LinkStackInit_LinkStack()

{returnNULL;

}

⑵判栈空

intEmpty_LinkStack(LinkStacktop)

{if(top==-1)return1;

elsereturn0;

}

⑶入栈

LinkStackPush_LinkStack(LinkStacktop,datatypex)

{StackNode*s;

s=malloc(sizeof(StackNode));

s->data=x;

s->next=top;

top=s;

returntop;

}

⑷出栈

LinkStackPop_LinkStack(LinkStacktop,datatype*x)

{StackNode*p;

if(top==NULL)returnNULL;

else{*x=top->data;

p=top;

top=top->next;

free(p);

returntop;

}

}

3.2栈的应用举例

由于栈的“先进先出”特点,在很多实际问题中都利用栈做一个辅助的数据结构来进行求解,下面通过几个例子进行说明。

例3.1简单应用:

数制转换问题

将十进制数N转换为r进制的数,其转换方法利用辗转相除法:

以N=3456,r=8为例转换方法如下:

NN/8(整除)N%8(求余)

34674333低

433541

5466

606高

所以:

(3456)10=(6563)8

我们看到所转换的8进制数按底位到高位的顺序产生的,而通常的输出是从高位到低位的,恰好与计算过程相反,因此转换过程中每得到一位8进制数则进栈保存,转换完毕后依次出栈则正好是转换结果。

算法思想如下:

当N>0时重复1,2

1.若N≠0,则将N%r压入栈s中,执行2;若N=0,将栈s的内容依次出栈,算法结束。

2.用N/r代替N

算法如下:

typedefintdatatype;#defineL10

voidconversion(intN,intr)voidconversion(intN,intr)

{SeqStacks;{ints[L],top;/*定义一个顺序栈*/

datetypex;intx;

Init_SeqStack(&s);top=-1;/*初始化栈*/

while(N)while(N)

{Push_SeqStack(&s,N%r);{s[++top]=N%r;/*余数入栈*/

N=N/r;N=N/r;/*商作为被除数继续*/

}}

while(Empty_SeqStack(&s))while(top!

=-1)

{Pop_SeqStack(&s,&x);{x=s[top--];

printf(“%d”,x);printf(“%d”,x);

}}

}}

算法3.1(a)算法3.1(b)

算法3.1(a)是将对栈的操作抽象为模块调用,使问题的层次更加清楚。

而算法3.1(b)中的直接用int向量S和int变量top作为一个栈来使用,往往初学者将栈视为一个很复杂的东西,不知道如何使用,通过这个例子可以消除栈的“神秘”,当应用程序中需要一个与数据保存时相反顺序使用数据时,就要想到栈。

通常用顺序栈较多,因为很便利。

在后面的例子中,为了在算法中表现出问题的层次,有关栈的操作调用了的相关函数,如象算法3.1(a)那样,对余数的入栈操作:

Push_SeqStack(&s,N%r);因为是用c语言描述,第一个参数是栈的地址才能对栈进行加工。

在后面的例子中,为了算法的清楚易读,在不至于混淆的情况下,不再加地址运算符,请读者注意。

例3.2利用栈实现迷宫的求解。

问题:

这是实验心理学中的一个经典问题,心理学家把一只老鼠从一个无顶盖的大盒子的入口处赶进迷宫。

迷宫中设置很多隔壁,对前进方向形成了多处障碍,心理学家在迷宫的唯一出口处放置了一块奶酪,吸引老鼠在迷宫中寻找通路以到达出口。

求解思想:

回溯法是一种不断试探且及时纠正错误的搜索方法。

下面的求解过程采用回溯法。

从入口出发,按某一方向向前探索,若能走通(未走过的),即某处可以到达,则到达新点,否则试探下一方向;若所有的方向均没有通路,则沿原路返回前一点,换下一个方向再继续试探,直到所有可能的通路都探索到,或找到一条通路,或无路可走又返回到入口点。

在求解过程中,为了保证在到达某一点后不能向前继续行走(无路)时,能正确返回前一点以便继续从下一个方向向前试探,则需要用一个栈保存所能够到达的每一点的下标及从该点前进的方向。

需要解决的四个问题:

1.表示迷宫的数据结构:

设迷宫为m行n列,利用maze[m][n]来表示一个迷宫,maze[i][j]=0或1;其中:

0表示通路,1表示不通,当从某点向下试探时,中间点有8个方向可以试探,(见图3.4)而四个角点有3个方向,其它边缘点有5个方向,为使问题简单化我们用maze[m+2][n+2]来表示迷宫,而迷宫的四周的值全部为1。

这样做使问题简单了,每个点的试探方向全部为8,不用再判断当前点的试探方向有几个,同时与迷宫周围是墙壁这一实际问题相一致。

如图3.4表示的迷宫是一个6×8的迷宫。

入口坐标为(1,1),出口坐标为(m,n)。

入口(1,1)

0

1

2

3

4

5

6

7

8

9

0

1

1

1

1

1

1

1

1

1

1

1

1

0

1

1

1

0

1

1

1

1

2

1

1

0

1

0

1

1

1

1

1

3

1

0

1

0

0

0

0

0

1

1

4

1

0

1

1

1

0

1

1

1

1

5

1

1

0

0

1

1

0

0

0

1

6

1

0

1

1

0

0

1

1

0

1

7

1

1

1

1

1

1

1

1

1

1

出口(6,8)

图3.4用maze[m+2][n+2]表示的迷宫

迷宫的定义如下:

#definem6/*迷宫的实际行*/

#definen8/*迷宫的实际列*/

intmaze[m+2][n+2];

2.试探方向:

在上述表示迷宫的情况下,每个点有8个方向去试探,如当前点的坐标(x,y),与其相邻的8个点的坐标都可根据与该点的相邻方位而得到,如图3.5所示。

因为出口在(m,n),因此试探顺序规定为:

从当前位置向前试探的方向为从正东沿顺时针方向进行。

为了简化问题,方便的求出新点的坐标,将从正东开始沿顺时针进行的这8个方向的坐标增量放在一个结构数组move[8]中,在move数组中,每个元素有两个域组成,x:

横坐标增量,y:

纵坐标增量。

move数组如图3.6所示。

Move数组定义如下:

typedefstruct

{intx,y

}item;

itemmove[8];

这样对move的设计会很方便地求出从某点(x,y)按某一方向v(0<=v<=7)到达的新点(i,j)的坐标:

i=x+move[v].x;j=y+move[v].y;

x

y

0

0

1

1

1

1

2

1

0

3

1

-1

4

0

-1

5

-1

-1

6

-1

0

7

-1

1

图3.6增量数组move

3.栈的设计:

当到达了某点而无路可走时需返回前一点,再从前一点开始向下一个方向继续试探。

因此,压入栈中的不仅是顺序到达的各点的坐标,而且还要有从前一点到达本点的方向。

对于图3.4所示迷宫,依次入栈为:

top—>

5,8,2

  

5,7,0

5,6,0

4,5,1

top—>

3,6,0

3,6,3

3,5,0

3,5,0

3,4,0

3,4,0

3,3,0

3,3,0

2,2,1

2,2,1

1,1,1

1,1,1

 

栈中每一组数据是所到达的每点的坐标及从该点沿哪个方向向下走的,对于图3.4迷宫,走的路线为:

(1,1)1(2,2)1(3,3)0(3,4)0(3,5)0(3,6)0(下脚标表示方向),当从点(3,6)沿方向0到达点(3,7)之后,无路可走,则应回溯,即退回到点(3,6),对应的操作是出栈,沿下一个方向即方向1继续试探,方向1、2试探失败,在方向3上试探成功,因此将(3,6,3)压入栈中,即到达了(4,5)点。

栈中元素是一个由行、列、方向组成的三元组,栈元素的设计如下:

typedefstruct

{intx,y,d;/*横纵坐标及方向*/

}datatype;

栈的定义仍然为:

SeqStacks;

4.如何防止重复到达某点,以避免发生死循环:

一种方法是另外设置一个标志数组mark[m][n],它的所有元素都初始化为0,一旦到达了某一点(i,j)之后,使mark[i][j]置1,下次再试探这个位置时就不能再走了。

另一种方法是当到达某点(i,j)后使maze[i][j]置-1,以便区别未到达过的点,同样也能起到防止走重复点的目的,本书采用后者方法,算法结束前可恢复原迷宫。

迷宫求解算法思想如下:

1.栈初始化;

2.将入口点坐标及到达该点的方向(设为-1)入栈

3.while(栈不空)

{栈顶元素=>(x,y,d)

出栈;

求出下一个要试探的方向d++;

while(还有剩余试探方向时)

{if(d方向可走)

则{(x,y,d)入栈;

求新点坐标(i,j);

将新点(i,j)切换为当前点(x,y);

if((x,y)==(m,n))结束;

else重置d=0;

}

elsed++;

}

}

算法如下:

intpath(maze,move)

intmaze[m][n];

itemmove[8];

{SeqStacks;

datetypetemp;

intx,y,d,i,j;

temp.x=1;temp.y=1;temp.d=-1;

Push_SeqStack(s,temp);

while(!

Empty_SeqStack(s))

{Pop_SeqStack(s,&temp);

x=temp.x;y=temp.y;d=temp.d+1;

while(d<8)

{i=x+move[d].x;j=y+move[d].y;

if(maze[i][j]==0)

{temp={x,y,d};

Push_SeqStack(s,temp);

x=i;y=j;maze[x][y]=-1;

if(x==m&&y==n)return1;/*迷宫有路*/

elsed=0;

}

elsed++;

}/*while(d<8)*/

}/*while*/

return0;/*迷宫无路*/

}

算法3.2

栈中保存的就是一条迷宫的通路。

例3.3表达式求值

表达式求值是程序设计语言编译中一个最基本的问题。

它的实现也是需要栈的加入。

下面的算法是由算符优先法对表达式求值。

表达式是由运算对象、运算符、括号组成的有意义的式子。

运算符从运算对象的个数上分,有单目运算符和双目运算符;从运算类型上分,有算术运算、关系运算、逻辑运算。

在此仅限于讨论只含二目运算符的算术表达式。

1.中缀表达式求值:

中缀表达式:

每个二目运算符在两个运算量的中间,假设所讨论的算术运算符包括:

+、-、*、/、%、^(乘方)和括号()。

设运算规则为:

.运算符的优先级为:

()——>^ ——>*、/、%——> +、- ;

.有括号出现时先算括号内的,后算括号外的,多层括号,由内向外进行;

.乘方连续出现时先算最右面的;

表达式作为一个满足表达式语法规则的串存储,如表达式“3*2^(4+2*2-1*3)-5”,它的的求值过程为:

自左向右扫描表达式,当扫描到3*2时不能马上计算,因为后面可能还有更高的运算,正确的处理过程是:

需要两个栈:

对象栈s1和算符栈s2。

当自左至右扫描表达式的每一个字符时,若当前字符是运算对象,入对象栈,是运算符时,若这个运算符比栈顶运算符高则入栈,继续向后处理,若这个运算符比栈顶运算符低则从对象栈出栈两个运算量,从算符栈出栈一个运算符进行运算,并将其运算结果入对象栈,继续处理当前字符,直到遇到结束符。

根据运算规则,左括号“(”在栈外时它的级别最高,而进栈后它的级别则最低了;乘方运算的结合性是自右向左,所以,它的栈外级别高于栈内;就是说有的运算符栈内栈外的级别是不同的。

当遇到右括号“)”时,一直需要对运算符栈出栈,并且做相应的运算,直到遇到栈顶为左括号“(”时,将其出栈,因此右括号“)”级别最低但它是不入栈的。

对象栈初始化为空,为了使表达式中的第一个运算符入栈,算符栈中预设一个最低级的运算符“(”。

根据以上分析,每个运算符栈内、栈外的级别如下:

算符栈内级别栈外级别

^34

*、/、%22

+、-11

(04

)-1-1

中缀表达式表达式“3*2^(4+2*2-1*3)-5”求值过程中两个栈的状态情况见图3.7所示。

读字符对象栈s1算符栈s2说明

3

3

3入栈s1

*

3

(*

*入栈s2

2

3,2

(*

2入栈s1

^

3,2

(*^

^入栈s2

3,2

(*^(

(入栈s2

4

3,2,4

(*^(

4入栈s1

+

3,2,4

(*^(+

+入栈s2

2

3,2,4,2

(*^(+

2入栈s1

*

3,2,4,2

(*^(+*

*入栈s2

2

3,2,4,2,2

(*^(+*

2入栈s1

-

3,2,4,4

(*^(+

做2+2=4,结果入栈s1

3,2,8

(*^(

做4+4=8,结果入栈s2

3,2,8

(*^(-

-入栈s2

3,2,8,1

(*^(-

1入栈s1

*

3,2,8,1

(*^(-*

*入栈s2

3

3,2,8,1,3

(*^(-*

3入栈s1

3,2,8,3

(*^(-

做1*3,结果3入栈s1

3,2,5

(*^(

做8-3,结果5入栈s2

3,2,5

(*^

(出栈

-

3,32

(*

做2^5,结果32入栈s1

96

做3*32,结果96入栈s1

96

(-

-入栈s2

5

96,5

(-

5入栈s1

结束符

91

做96-5,结果91入栈s1

图3.7中缀表达式3*2^(4+2*2-1*3)-5的求值过程

为了处理方便,编译程序常把中缀表达式首先转换成等价的后缀表达式,后缀表达式的运算符在运算对象之后。

在后缀表达式中,不在引入括号,所有的计算按运算符出现的顺序,严格从左向右进行,而不用再考虑运算规则和级别。

中缀表达式“3*2^(4+2*2-1*3)-5”的后缀表达式为:

“32422*+13*-^*5-”。

2.后缀表达式求值

计算一个后缀表达式,算法上比计算一个中缀表达式简单的多。

这是因为表达式中即无括号又无优先级的约束。

具体做法:

只使用一个对象栈,当从左向右扫描表达式时,每遇到一个操作数就送入栈中保存,每遇到一个运算符就从栈中取出两个操作数进行当前的计算,然后把结果再入栈,直到整个表达式结束,这时送入栈顶的值就是结果。

下面是后缀表达式求值的算法,在下面的算法中假设,每个表达式是合乎语法的,并且假设后缀表达式已被存入一个足够大的字符数组A中,且以‘#’为结束字符,为了简化问题,限定运算数的位数仅为一位且忽略了数字字符串与相对应的数据之间的转换的问题。

typedefchardatetype;

doublecalcul_exp(char*A)

{/*本函数返回由后缀表达式A表示的表达式运算结果*/

Seq_Starcks;

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

while(ch!

=’#’)

{

if(ch!

=运算符)Push_SeqStack(s,ch);

else{Pop_SeqStack(s,&a);

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

switch(ch).

{casech==’+’:

c=a+b;break;

casech==’-’:

c=a-b;break

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 职业教育 > 其它

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1