栈的教程.docx

上传人:b****7 文档编号:9515955 上传时间:2023-02-05 格式:DOCX 页数:21 大小:58.53KB
下载 相关 举报
栈的教程.docx_第1页
第1页 / 共21页
栈的教程.docx_第2页
第2页 / 共21页
栈的教程.docx_第3页
第3页 / 共21页
栈的教程.docx_第4页
第4页 / 共21页
栈的教程.docx_第5页
第5页 / 共21页
点击查看更多>>
下载资源
资源描述

栈的教程.docx

《栈的教程.docx》由会员分享,可在线阅读,更多相关《栈的教程.docx(21页珍藏版)》请在冰豆网上搜索。

栈的教程.docx

栈的教程

第3章栈和队列

    栈和队列是两种重要的线性结构。

从数据结构角度看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集,它们是操作受限的线性表,因此,可称为限定性的数据结构。

但从数据类型角度看,它们是和线性表大不相同的两类重要的抽象数据类型。

由于它们广泛应用在各种软件系统中,因此在面向对象的程序设计中,它们是多型数据类型。

本章除了讨论栈和队列的定义、表示方法和实现外,还将给出一些应用的例子。

3.1抽象数据类型栈的定义

栈(stack)是限定仅在表尾进行插入或删除操作的线性表。

因此,对栈来说,表尾端有其特殊含义,称为栈顶(top),相应地,表头端称为栈底(bottom)。

不含元素的空表称为空栈。

   假设栈

,则称

为栈底元素,

为栈顶元素。

栈中元素按

的次序进栈,退栈的第一个元素应为栈顶元素。

换句话说,栈的修改是按后进先出的原则进行的(如图3.1(a)所示)。

因此,栈又称为后进先出(lastinfirstout)的线性表(简称LIFO结构),它的这个特点可用图3.1(b)所示的铁路调度站形象地表示。

   栈的基本操作除了在栈顶进行插入或删除外,还有栈的初始化、判空及取栈顶元素等。

下面给出栈的抽象数据类型的定义:

ADTStack{

   数据对象:

D={

|

∈ElemSet,i=1,2,…,n,n≥0}

   数据关系:

R1={<

>|

∈D,i=2,…,n}

              约定

端为栈顶,

端为栈底。

   基本操作:

        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()失败,则操作失效。

}ADTStack

   本书在以后各章中引用的栈大多为如上定义的数据类型,栈的数据元素类型在应用程序内定义,并称插入元素的操作为入栈,删除栈顶元素的操作为出栈。

.1.2 栈的表示和实现

    和线性表类似,栈也有两种存储表示方法。

    顺序栈,即栈的顺序存储结构是利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素,同时附设指针top指示栈顶元素在顺序栈中的位置。

通常的习惯做法是以top=0表示空栈,鉴于C语言中数组的下标约定从0开始,则当以C作描述语言时,如此设定会带来很大不便;另一方面,由于栈在使用过程中所需最大空间的大小很难估计,因此,一般来说,在初始化设空栈时不应限定栈的最大容量。

一个较合理的做法是:

先为栈分配一个基本容量,然后在应用过程中,当栈空间不够使用时再逐段扩大。

为此,可设定两个常量:

STACK_INIT_SIZE(存储空间初始分配量)和STACKINCREMENT(存储空间分配增量),并以下述类型说明作为顺序栈的定义。

typedefstruct{

  SElemType*base;

  SElemType*top;

  intstacksize;

}SqStack;

其中,stacksize指示栈的当前可使用的最大容量。

栈的初始化操作为:

按设定的初始分配量进行第一次存储分配,base可称为栈底指针,在顺序栈中,它始终指向栈底的位置,若base的值为NULL,则表明栈结构不存在。

称top为栈顶指针,其初值指向栈底,即top=base可作为栈空的标记,每当插入新的栈顶元素时,指针top增1;删除栈顶元素时,指针top减1,因此,非空栈中的栈顶指针始终在栈顶元素的下一个位置上。

图3.2展示了顺序栈中数据元素和栈顶指针之间的对应关系。

以下是顺序栈的模块说明。

//====ADTStack的表示与实现====

    //====栈的顺序存储表示====

#defineSTACK_INIT_SIZE100;//存储空间初始分配量

#defineSTACKINCREMENT10;   //存储空间分配增量

typedefstruct{

   SElemType*base; //在栈构造之前和销毁之后,base的值为NULL

   SElemType*top;  //栈顶指针

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

}SqStack;

//-----基本操作的函数原型说明-----

StatusInitStack(SqStack&S);

   //构造一个空栈S

StatusDestroyStack(SqStack&S);

   //销毁栈S,S不再存在

StatusClearStack(SqStack&S);

   //把S置为空栈

StatusStackEmpty(SqStackS);

   //若栈S为空栈,则返回TRUE,否则返回FALSE

intStackLength(SqStackS);

   //返回S的元素个数,即栈的长度

StatusGetTop(SqStackS,SElemType&e);

   //若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR

StatusPush(SqStack&S,SElemTypee);

   //插入元素e为新的栈顶元素

StatusPop(SqStack&S,SElemType&e);

   //若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR

StatusStackTraverse(SqStackS,Status(*visit)());

   //从栈底到栈顶依次对栈中每个元素调用函数visit()。

一旦visit()失败,则操作失败

   //-----基本操作的算法描述(部分)-----

StatusInitStack(SqStack&S){

   //构造一个空栈S

 S.base=(SElemType*)malloc(STACK_INIT_SIZE*sizeof(ElemType));

   if(!

S.base)exit(OVERFLOW);  //存储分配失败

   S.top=S.base;

   S.stacksize=STACK_INIT_SIZE;

   returnOK;

}//InitStack

StatusGetTop(SqStackS,SElemType&e){

   //若栈不空,则用e返回S的栈顶元素,并返回OK;否则返回ERROR

   if(S.top==S.base)returnERROR;

   e=*(S.top-1);

   returnOK;

}//GetTop

StatusPush(SqStack&S,SElemTypee){

   //插入元素e为新的栈顶元素

   if(S.top-S.base>=S.stacksize){//栈满,追加存储空间

      S.base=(ElemType*)realloc(S.base,(S.stacksize+STACKINCREMENT)*sizeof(ElemType));

      if(!

S.base)exit(OVERFLOW);//存储分配失败

      S.top=S.base+S.stacksize;

      S.stacksize+=STACKINCREMENT;

   }

   *S.top++=e;  //等价于*S.top=e;S.top++;

   returnOK;

}//Push

StatusPop(SqStack&S,SElemType&e){

  //若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回ERROR

  if(S.top==S.base)returnERROR;

  e=*--S.top;

  returnOK;

}//Pop

   栈的链式表示—链栈如图3.3所示。

由于栈的操作是线性表操作的特例,则链栈的操作易于实现,在此不作详细讨论。

栈的应用举例之数制转换

由于栈结构具有后进先出的固有特性,致使栈成为程序设计中的有用工具。

本节将讨论几个栈应用的典型例子。

3.2.1数制转换

    十进制数N和其他d进制数的转换是计算机实现计算的基本问题,其解决方法很多,其中一个简单算法基于下列原理:

   N=(Ndivd)×d+Nmodd(其中:

div为整除运算,mod为求余运算)

   例如:

,其运算过程如下:

           N           Ndiv8         Nmod8

        1348               168               4

         168                21                0

           21                  2                5

           2                  0                2

   假设现要编制一个满足下列要求的程序:

对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数。

由于上述计算过程是从低位到高位顺序产生八进制数的各个数位,而打印输出,一般来说应从高位到低位进行,恰好和计算过程相反。

因此,若将计算过程中得到的八进制数的各位顺序进栈,则按出栈序列打印输出的即为与输入对应的八进制数。

voidconversion(){

 //对于输入的任意一个非负十进制整数,打印输出与其等值的八进制数

 InitStack(S);  //构造空栈

  scanf("%d",N);

  while(N){

   Push(S,N%8);

   N=N/8;

 }

 while(!

StackEmpty(s)){

   Pop(S,e);

   printf("%d",e);

 }

}//conversion

算法3.1

   这是利用栈的后进先出特性的最简单的例子。

在这个例子中,栈操作的序列是直线式的,即先一味地入栈,然后一味地出栈。

也许,有的读者会提出疑问:

用数组直接实现不也很简单吗?

仔细分析上述算法不难看出,栈的引入简化了程序设计的问题,划分了不同的关注层次,使思考范围缩小了。

而用数组不仅掩盖了问题的本质,还要分散精力去考虑数组下标增减等细节问题。

栈的应用举例之括号匹配的检验

3.2.2括号匹配的检验

    假设表达式中允许包含两种括号:

圆括号和方括号,其嵌套的顺序随意,即([]())或[([][])]等为正确的格式,[(]或([())或(()])均为不正确的格式。

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

例如考虑下列括号序列:

   [([][])]

    12345678

   当计算机接受了第一个括号后,它期待着与其匹配的第八个括号的出现,然而等来的却是第二个括号,此时第一个括号“[”只能暂时靠边,而迫切等待与第二个括号相匹配的、第七个括号“)”的出现,类似地,因等来的是第三个括号“[”,其期待匹配的程度较第二个括号更急迫,则第二个括号也只能靠边,让位于第三个括号,显然第二个括号的期待急迫性高于第一个括号;在接受了第四个括号之后,第三个括号的期待得到满足,消解之后,第二个括号的期待匹配就成为当前最急迫的任务了,……,依次类推。

可见,这个处理过程恰与栈的特点相吻合。

由此,在算法中设置一个栈,每读入一个括号,若是右括号,则或者使置于栈顶的置于栈顶的最急迫的期待得以消解,或者是不合法的情况;若是左括号,则作为一个新的更急迫的期待压入栈中,自然使原有的在栈中的所有未消解的期待的急迫性都降了一级。

另外,在算法的开始和结束时,栈都应该是空的。

栈的应用举例之表达式求值

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

它的实现是栈应用的又一个典型例子。

这里介绍一种简单直观、广为使用的算法,通常称为“算符优先法”。

   要把一个表达式翻译成正确求值的一个机器指令序列,或者直接对表达式求值,首先要能够正确解释表达式。

例如,要对下面的算术表达式求值:

   4+2×3-10/5

首先要了解算术四则运算的规则。

即:

   

(1)先乘除,后加减;

  

(2)从左算到右;

  (3)先括号内,后括号外。

由此,这个算术表达式的计算顺序应为

   4+2×3-10/5=4+6-10/5=10-10/5=10-2=8

算符优先法就是根据这个运算优先关系的规定来实现对表达式的编译或解释执行的。

   任何一个表达式都是由操作数(operand)、运算符(operator)和界限符(delimiter)组成的,我们称它们为单词。

一般地,操作数既可以是常数也可以是被说明为变量或常量的标识符;运算符可以分为算术运算符、关系运算符和逻辑运算符3类;基本界限符有左右括号和表达式结束符等。

为了叙述的简洁,我们仅讨论简单算术表达式的求值问题。

这种表达式只含加、减、乘、除4种运算符。

读者不难将它推广到更一般的表达式上。

   我们把运算符和界限符统称为算符,它们构成的集合命名为OP。

根据上述3条运算规则,在运算的每一步中,任意两个相继出现的算符θ1和θ2之间的优先关系至多是下面3种关系之一:

   θ1<θ2 θ1的优先权低于θ2

   θ1=θ2 θ1的优先权等于θ2

   θ1>θ2 θ1的优先权高于θ2

表1定义了算符之间的这种优先关系。

表1算符间的优先关系

  由规则(3),+、-、*和/为θ1时的优先性均低“(”但高于“)”,由规则

(2),当θ1=θ2时,令θ1>θ2,“#”是表达式的结束符。

为了算法简洁,在表达式的最左边也虚设一个“#”构成整个表达式的一对括号。

表中的“(”=“)”表示当左右括号相遇时,括号内的运算已经完成。

同理,“#”=“#”表示整个表达式求值完毕。

“)”与“(”、“#”与“)”以及“(”与“#”之间无优先关系,这是因为表达式中不允许它们相继出现,一旦遇到这种情况,则可以认为出现了语法错误。

在下面的讨论中,我们暂假定所输入的表达式不会出现语法错误。

   为实现算符优先算法,可以使用两个工作栈。

一个称做OPTR,用以寄存运算符;另一个称做OPND,用以寄存操作数或运算结果。

算法的基本思想的:

  

(1)首先置操作数栈为空栈,表达式起始符“#”为运算符栈的栈底元素;

  

(2)依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为“#”)。

   以下算法描述了这个求值过程。

OperandTypeEvaluateExpression(){

  //算术表达式求值的算符优先算法。

设OPTR和OPND分别为运算符栈和运算数栈,OP为运算符集合。

  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;

        }//switch

  }//while

  returnGetTop(OPND);

}//EvaluateExpression

   算法中还调用了两个函数。

其中Precede是判定运算符栈的栈顶运算符θ1与读入的运算符θ2之间优先关系的函数;Operate为进行二元运算aθb的函数,如果是编译表达式,则产生这个运算的一组相应指令并返回存放结果的中间变量名;如果是解释执行表达式,则直接进行该运算,并返回运算的结果。

   例利用算法EvaluateExpression_reduced对算术表达式3*(7-2)求值,操作过程如下所示。

步骤

OPTR栈

OPND栈

输入字符

主要操作

1

      #

 

3*(7-2)#

 PUSH(OPND,'3')

2

      #

     3

 *(7-2)#

 PUSH(OPTR,'*')

3

      #*

     3

   (7-2)#

 PUSH(OPTR,'(')

4

      #*(

     3

     7-2)#

 PUSH(OPND,'7')

5

      #*(

     37

       -2)#

 PUSH(OPTR,'-')

6

      #*(-

     37

         2)#

 PUSH(OPND,'2')

7

      #*(-

     372

           )#

 operate('7','-','2')

8

      #*(

     35

           )#

 POP(OPTR){消去一对括号}

9

      #*

     35

            #

 operate('3','*','5')

10

      #

     15

            #

 RETURN(GETTOP(OPND))

栈与递归的实现

(一)

栈还有一个重要应用是在程序设计语言中实现递归。

一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。

   递归是程序设计中一个强有力的工具。

其一,有很多数学函数是递归定义的,如大家熟悉的阶乘函数

   2阶Fibonacci数列

   和Ackerman函数

等;其二,有的数据结构,如二叉树、广义表等,由于结构本身固有的递归特性,则它们的操作可递归地描述;其三,还有一类问题,虽然问题本身没有明显的递归结构,但用递归求解比迭代求解更简单,如八皇后问题、Hanoi塔问题等。

栈与递归的实现

(二)

例3-2 (n阶Hanoi塔问题)假设有3个分别命名为X、Y和Z的塔座,在塔座X上插有n个直径大小各不相同、依小到大编号为1,2,…,

n的圆盘(如图3.5所示)。

现要求将X轴上的n个圆盘移至塔座Z上并仍按同样顺序叠排,圆盘移动时必须遵循下列规则:

   

(1)每次只能移动一个圆盘;

   

(2)圆盘可以插在X、Y和Z中的任一塔座上;

                           (3)任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。

   如何实现移动圆盘的操作呢?

当n=1时,问题比较简单,只要将编号为1的圆盘从塔座X直接移至塔座Z上即可;当n>1时,需利用塔座Y作辅助塔座,若能设法将压在编号为n的圆盘之上的n-1个圆盘从塔座X(依照上述法则)移至塔座Y上,则可先将编号为n的圆盘从塔座X移至塔座Z上,然后再将塔座Y上的n-1个圆盘(依照上述法则)移至塔座Z上。

而如何将n-1个圆盘从一个塔座移至另一个塔座的问题是一个和原问题具有相同特征属性的问题,只是问题的规模小1,因此可以用同样的方法求解。

由此可得如算法3.5所示的求解n阶Hanoi塔问题的C函数。

voidhanoi(intn,charx,chary,charz)

   //将塔座x上按直径由小到大且自上而下编号为1至n的n个圆盘按规则搬到塔座z上,y可用作辅助塔座。

   //搬动操作move(x,n,z)可定义为(c是初值为0的全局变量,对搬动计数):

   //printf("%i.Movedisk%ifrom %c to%c\n",++c,n,x,z);

 1{

 2   if(n==1)

 3      move(x,1,z);  //将编号为1的圆盘从x移到z

 4   else{

 5      h

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

当前位置:首页 > 党团工作 > 入党转正申请

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

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