中缀和后缀算术表达式的分析比较

 

中缀和后缀算术表达式的分析比较

刘爱贵

(高能物理研究所计算中心 北京 2003年)

摘要     表达式求值 是程序设计语言编译中的一个最基本问题。与人们习惯的中缀表示的表达式相比,后缀表达式 不存在括号,没有优先级的差别,表 达式中各个运算是按照运算符出现的顺序进行的。因此非常适合串行工作的计算机处理方式。本文首先对这两种表达式表示方法进行了分析比较,然后通过具体分析实现这两种表达式求值的算法来论证表达式后缀表示优于中缀表示。最后简要谈了一下中缀表达式到后缀表达式的转换。

关键词 中缀表达式 后缀表达式 前缀表达式 优先级 算符优先 堆栈 表达式转换 时间复杂度

 

表达式求值是程序设计语言编译中的一个最基本问题。 通常书写的算术表达式是由操作数和运算符以及改变运算次序的圆括号连接而成的式子。运算符包括单目运算符和双目运算符两类,单目运算符只要求一个操作数,并被放在该操作数的前面,双目运算符要求有两个操作数,其位置因表示方法不同而有所差异。按照运算符与运算对象的位置关系,算术表达式的表示方法分为前缀表达式、中缀表达式和事缀表达式三种,其中后两者较为常用。 为了简便起见,在本文的讨论中只考虑双目运算符(仅+、-、*、/  四种)以及括号。

中缀算术表达式最为常用,其 二元运算符置于与之相关的两个运算对象之间。对中缀表达式的计值,并非按照运算符出现的自然顺序来执行其中的各个运算,而是根据算符间的优先关系来确定运算的次序,此外,还需要顾及括号规则。中缀表达式的计算比较复杂,它必须遵守以下三条规则[1]

(1) 先计算括号内,后计算括号外;

(2) 在无括号或同层括号内,先乘除运算,后加减运算,即乘除运算的优先级高于加减运算的优先级;

(3) 同一优先级运算,从左向右依次进行。

从这三条规则可以看出,在中缀表达式的计算过程中,既要考虑括号的作用,又要考虑运算符的优先级,还要考虑运算符出现的先后次序。因此,各运算符实际的运算次序往往同它们在表达式中出现的先后次序是不一致的,是不可预测的。中缀算术表达式符合人的思维方式,我们 凭直观判别一个中缀表达式中运算符运算顺序并不困难,但对于计算机处理起来就比较复杂了。由于传统计算机一维的计算模型,只能一个字符一个字符地扫描,要想得到哪一个运算符先算,就必须对整个中缀表达式扫描一遍,一个中缀表达式中有多少个运算符,原则上就得扫描多少遍才能计算完毕,这样算法的时间复杂性就差了,显然是不可取的。

波兰逻辑学家J.Lukasiewicz1929 年提出了后缀表达式的表示方法。这种方法的算术表达式中的每一个运算符都置于其运算对象之后,表达式中各个运算是按照运算符出现的顺序进行的,故无须使用括号来指示运算顺序,因而又称为无括号式。[2] 如中缀表达式100+(200-50*3)/(13-8) ,其后缀表达式为100 200 50 3 * - 13 8 - / + 。 在后缀表达式中,不存在括号,也不存在优先级的差别,计算过程完全按照运算符出现的先后次序进行,整个计算过程仅需一遍扫描便可完成,显然比中缀表达式的计算要简单得多。其实 J.Lukasiewicz 原来提出的是表达式的前缀表示方法,即把每一运算符置于运算对象之前[2] , 前面的表达式用前缀表示为+ 100 * 50 3 - / 200 20 。前缀表达式的优点与后缀表达式的相同,也不含有括号,表达式中的运算也是按照运算符出现的顺序进行的,计值也很容易实现。但由于其表示方法与人们的习惯相差甚远,因而并不常用。

对于简单的中缀表达式我们很容易得到其后缀表达式,但对于较为复杂的中缀表达式就很难从直观上得到其后缀表达式。我们可以用一棵二叉树来表示算术表达式,内部结点表示运算符,叶结点代表运算对象,如100+(200-50*3)/(13-8) 。这样按照先序、中序、后序遍历二叉树,就可以分别得到前缀、中缀和后缀算术表达式。如此可以很方便地实现三种算术表达式的相互转换。

 

                                                  

二叉树表示的算术表达式 100+(200-50*3)/(13-8)

 

在计算机中进行算术表达式的计算是通过堆栈来实现的。后缀表达式由于其本身所具有的优点,表达式中各个运算是按照运算符出现的顺序进行的, 其计算求值比较简单,扫描一遍即可完成。它只需要使用一个栈,用来存储后缀表达式中的操作数、计算过程中的中间结果以及最后结果。后缀表达式求值算法的基本思路是:把包含后缀算术表达式的字符串定义为一个输入字符串,按顺序从中读取字符(空格作为数据之间的分隔符,不会被作为字符读入)时,若是运算符,则表明它的两个操作数已在栈中,其中栈顶元素为运算符的后一个操作数,栈顶元素的下一个元素为运算符的前一个操作数,把两个操作数出栈进行相应运算即可,然后把运算结果再压入栈中;否则,读入的字符必为操作数字符串中的字符,读取整个操作数字符串,转换成数后并把它压入到栈中。依次扫描每一个字符并进行上述处理,直到遇到结束符 # 为止,表明后缀表达式计算完毕,最终结果保存在栈中,并且栈中仅存这一个值,把它弹出返回即可。后缀表达式求值算法描述为:

int postexpression(char *exp)

{

   stack<int> *opnd=new(stack<int>);// 操作数栈

   char ch=*exp;

   int x=0,y,z,flag=FALSE;

   int result;

   while(ch!='#')

   {

       if(!Operator(ch))              // 如果当前字符不是运算符

       {

           if(ch!=' ')

           {

              x=x*10+(int)(ch)-48;     // 计算操作数

              flag=TRUE;

           }

           else

           {

             if(flag)opnd->push(x);   // 操作数入栈

              x=0;

              flag=FALSE;

           }

       }

       else                // 当前字符为运算符,两个操作数出栈,计算并将结果入栈

       {

           x=opnd->pop();

           y=opnd->pop();

           z=calc(y,ch,x);

           opnd->push(z);

       }

       ch=*(++exp);          // 取下一字符

   }

   result=opnd->pop();

   return(result);

}

    此算法的运行时间主要花在while 循环上,它从头到尾扫描后缀表达式中的每一个数据(每个操作数或运算符均为一个数据),若后缀表达式由n 个数据组成,则此算法的时间复杂度为O(n) 。此算法在运行时所占用的临时空间主要取决于操作数栈的大小,显然,它的最大深度不会超过表达式中操作数的个数,因为操作数的个数与运算符(假定把 # 也看作为一个特殊运算符,即结束运算符)的个数相等,所以此算法的空间复杂度也同样为O(n)

    中缀表达式求值同样用堆栈来实现,但实现相对后缀表达式较为复杂,它采用一种称为“算符优先法”的算法[1] ,它必须严格按照前面所述的三条规则来进行计算。为了实现算符优先算法,要使用两个工作栈。一个称为OPTR, 用以寄存运算符;另一个称为OPND ,用以寄存操作数或运算结果,它的基本思想于后缀表达式计算的不同在于:当读取到运算符时并不可能作相应运算,必须先比较运算符栈中栈顶元素与当前运算符的优先级。若为‘< ’则运算符入栈;若为‘= ’则说明是一对括号,需脱括号;若‘> ’则作相应运算并将结果入栈。

  中缀表达式求值算法描述如下:

  int middexpression(char *exp)

{

    stack<int> *opnd=new(stack<int>);   // 操作数栈

    stack<char> *optr=new(stack<char>);  // 运算符栈

    char ch=*exp;

    int x=0,y,z;

    int result;

    optr->push('#');

    while(ch!='#'||optr->gettop()!='#')// 字符扫描完毕且运算符栈仅有‘#’时运算结束

    {

        if(!Operator(ch))      // 取操作数并入栈

        {

           x=x*10+(int)(ch)-48;

           if(Operator(*(exp+1)))

           {

              opnd->push(x);

              x=0;

           }

           ch=*++exp;

        }

        else

        {

            switch(Precede(optr->gettop(),ch))

            {

            case '<':// 栈顶元素优先权低

                optr->push(ch);

                ch=*++exp;

                break;

            case '=':// 脱括号并接收下一字符

                optr->pop();

                ch=*++exp;

                break;

            case '>':// 退栈并将运算结果入栈,但不取下一表达式字符

                x=opnd->pop();

                y=opnd->pop();

                z=calc(y,optr->pop(),x);

                opnd->push(z);x=0;

                break;

            }

        }

    }

    result=opnd->pop();

    return(result);

}

分析上面算法,运算时间主要用在字符串扫描和算符优先权的比较上。把# 看作运算符,操作数与运算符个数相同,最坏情况下优先级比较是n/2 次,即运算顺序完全是逆序的, 每个字符扫描一遍是O(n) 的,所以整个算法复杂度是O(n2 ) 的。算法中用到两个栈,分别为O(n/2) ,其算法空间复杂度是O(n) 的。

从两个算法的分析比较中,我们可以看到优先级的比较、运算不按顺序进行使中缀表达计算的时间复杂性从O(n) 降到了O(n2 ), 花费了大量的运行时间。在Pentium II  266MHz128M RAMWin2000Vc6.0 测试环境下, 对中缀表达式100+(200-(50*(8-45/9)))17*9/3+13-11100+(200-50*3)/(13-8) 和其后缀表达式表达100 200 50 8 45 9 / - * - +17 9 * 3 / 13 + 11 –100 200 50 3 * - 13 8 - / + 分别调用middexpression()postexpression() ,三个表达式运算时间() 分别为(0.008681,0.003974 )、(0.005812,0.003663 )、(0.008437,0.004524 )。第一个表达式中缀表示运算是完全逆序的,其运算时间与后缀表示运算时间之比为2.1843321 ;第二个表达式中缀表示运算是完全顺序的,其运算时间与后缀表示运算时间之比为1.5866671 ;第三个表达式中缀表达运算是一般情况,其运算时间与后缀表示运算时间之比为1.8650471 。从中我们发现,后缀表达式运算时间比较稳定,而中缀表达式由于运算顺序不同运行时间相差较大。可见在时间复杂度和效率方面后缀表示方法要明显优于中缀表示方法。

经以上的分析比较可以发现,后缀表达式要优于中缀表达式,非常适合用计算机求解。但是比起中缀表达式,形式上没有中缀表达直观,后者更适于人类的思维方式。因此利用计算机进行科学计算,在高级语言程序设计中我们用中缀表达式来表示,而在计算机内部是用后缀表达式来进行计算的,因此为了处理方便,编译程序常把中缀表达式首先转换为后缀表达式然后再进行运算。中缀表达式-后缀表达式转换算法的思想与中缀表达式求值算法基本相似,这里就不再赘述。但值得一提的是,转换的时间复杂度是On )的,且只需要一个O(n/2) 的运算符栈。利用表达式的后缀表示和堆栈技术只需要两遍扫描就可完成中缀算术表达式的计算,时间复杂度仍是O(n) 的,显然要大大优于直接进行中缀算术表达式计算。

 

 

 

[ 参考文献]

[1] 严蔚敏、吴伟民     数据结构              1997.7  清华大学出版社

[2] 蒋立源         编译原理          1 993.8  西北工业大学出版社

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值