刁肥宅详解中缀表达式求值问题:C++实现顺序/链栈解决

       1. 表达式的种类

       如何将表达式翻译成能够正确求值的指令序列,是语言处理程序要解决的基本问题,作为栈的应用事例,下面介绍表达式的求值过程。 任何一个表达式都是由操作数(亦称运算对象)、操作符(亦称运算符)和分界符组成的。通常,算术表达式有3种表示: ①中缀(infix)表示:<操作数><操作符><操作数>,如A+B。 ②前缀(prefix)表示: <操作符><操作数><操作数>,如+AB。 ③后缀(postfix)表示: <操作数><操作数><操作符>,如AB+。

       2. 求解算法

       在中缀表达式中操作符的优先级和括号使得求值过程复杂化,把它转换成后缀表达式,可以简化求值过程。但是,老师使用的PPT上的示例显然是直接对中缀表达式进行求值,并未进行转换。因此我也采用“直接求值法”。为了实现对中缀表达式的求值,需要考虑各操作符的优先级,参见表1。

操作符ch # ( *、/、% +、- )
isp 0 1 5 3 6
icp 0 6 4 2 1

表1 各个运算符的优先级

  表中的isp叫做栈内优先级(in stack priority),icp叫做栈外优先级(in coming priority)。为什么要如此设置呢?这是因为某一操作符按照算术四则运算有一个优先级,这是icp。一旦它进入操作符栈,它的优先级要提高,以体现优先级相同的操作符先来的先做,就是说,在表达式中运算优先级相同的必须自左向右运算,这就是栈内优先级isp。
  从上表可以看到,左括号“(”的栈外优先数最高,它一来到立即进栈,但当它进入栈中后,其栈内优先数变得极低,以便括号内的其他操作符进栈。除它之外,其他操作符进入栈中后优先数都升1,这样可以体现中缀表示中相同优先级的操作符自左向右计算的要求,让位于栈顶的操作符先退栈输出。操作符优先数相等的情况只出现在“(”与栈内“)”括号配对或栈底的“#”号与表达式输入最后的“#”号配对时。前者将连续推出位于栈顶的操作符,直到遇到“)”为止。然后将“(”退栈以对消括号,后者将结束算法。
  扫描中缀表达式,并求值的算法描述如下:
  1)操作符栈初始化,将结束符“#”进栈;操作数栈初始化。然后读入中缀表达式字符流中的首字符 ch。
  2)重复执行以下步骤,直到 ch=“#”,同时栈顶的操作符也是“#”,停止循环。
  ①若 ch 是操作数,将压入操作数栈,读入下一个字符 ch。
  ②若 ch 是操作符,比较ch的优先级 icp 和操作符栈当前栈顶的操作符 op 的优先级 isp:
  • 若 icp(ch)> isp(op),令ch进栈,读入下一个字符ch。
  • 若 icp(ch)≤ isp(op),退出运算数栈的两个元素a和b计算:a op b,将计算结果压入运算数栈中。
  • 若 ch = “)”或 op = “(”,退出运算符栈的栈顶元素,读入下一字符ch。
  3)算法结束,运算数栈的栈顶元素即为所求中缀表达式的结果。
  以上算法是我结合在资料上看到的关于中缀表达式转换为后缀表达式的算法,自己琢磨出来的。算法的正确性有待证明,但实现在计算几个样例时,结果正误均有。具体结果将在第四部分展示。
  另外,我在博客和《算法导论》上都看到所谓“双栈算术表达式求值算法”的介绍与大概思路:
  双栈算术表达式求值算法是由E.W.Dijkstra在上个世纪60年代发明的一个很简单的算法,用两个栈【一个用来保存运算符、一个用来保存操作数】来完成对一个表达式的运算。其实整个算法思路很简单:
  • 无视左括号
  • 将操作数压入操作数栈
  • 将运算符压入运算符栈
  • 在遇到右括号的时候,从运算符栈中弹出一个运算符,再从操作数栈中弹出所需的操作数,并且将运算结果压入操作数栈中
---------------------
作者:erzhanchen
来源:CSDN
原文:https://blog.csdn.net/erzhanchen/article/details/57421267
版权声明:本文为博主原创文章,转载请附上博文链接!
为了表示对原作者的尊重,我保留了“犯罪痕迹”。

3. 核心代码

       在正式贴出核心代码(完整代码将写在下一篇博客)前先进行一些必要说明: ①从右向左扫描表达式字符串; ②C类型的字符串开始下标是0,字符串的最后一个元素是“\0”;

D a t a ' ' S t r u c t u r e '\0'
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

       ③假设“1”、“2”、“3”、“4”、“5”这五个数字组成一个十进制的整数,从左到右依次是万位数、千位数、百位数、十位数、个位数,那么计算这个整数的表达式就是下面这样的: 1×104 +2×103+3×102+4×101+5×100=12345

 1 void SeqStack::calculate( char* Str )///C类型字符串表达式为参数传入方法中
 2 {
 3     charSeqStack css1;///css1 用作操作符栈
 4     char ch1;///访问操作符栈栈顶元素时,保存栈顶元素
 5     int i = 0;///用来保存当前扫描的表达式位置
 6     double a, b;///用来保存临时运算时调用对象栈栈顶的前两个元素
 7     
 8     int cnt = 0;///记录字符串中数字连续时的位数,即计算 10 的 n 次方时幂的指数
 9     ///比方说:在读入整数156时,读“1”这个字符之前读了两个数字“5”、“6”,那么读1的时候,这个1就代表100的意思。
10     int temp = 0;///保存扫描到的数值
11  
12     while ( Str[i] != '\0' )///确定表达式的终止下标
13     {
14         i ++;
15     }///比方说:在扫描表达式“#1+1#”时,表达式字符串字符最大下标是5,但是表达式终止下标是3
16     i = i - 2;///C类型字符串下标从0开始,字符串的最后一位是'\0'
17     
18     ///从右向左扫描表达式,当 操作符栈不为空 或 未扫描到表达式的最左边 时 循环
19     while( css1.topValue() != -1 || Str[i] != '#' )
20     {
21         char ch = Str[i];///读取扫描到的当前字符
22         if ( isdigit(ch) )///利用C语言的库函数 isdigit() 判断字符 ch 是否为数字。
23         ///库函数 isdigit() 声明在头文件 ctype.h 中
24         ///算法步骤 2)的情况 ①,如果 ch 是数字就将其转换为对应的十进制数
25         {
26             temp = temp + pow( 10, cnt ) * int( ch - '0' );///类似于 m 进制 转换为 10 进制数时的操作
27             /// C语言的 pow() 函数定义在 math.h 中,pow( 10, cnt ) 计算的是 10 的 cnt 次方
28             cnt ++;///位数增一
29             i --;///继续向左扫描表达式
30         }
31         else///如果 ch 不是数字,是运算符
32         {
33             if (cnt)///C语言中非零值的表达式布尔值都为真,如果这个表达式为真说明原来扫描到了表达式中的数值
34             {
35                 push(temp);///将数值压入操作数栈中,操作数栈是调用对象中的栈
36                 temp = 0;///在未扫描到新的数字前置零
37                 cnt = 0;///在未扫描到新的数字前位数置零
38             }
39             css1.getTop(ch1);///读取操作符栈的栈顶
40             if ( ch1 == ')' && ch == '(' )///算法步骤 2)的情况 ② 的第三种
41             {
42                 css1.pop();///将运算符栈栈顶元素退出
43                 i --;///继续向左扫描表达式
44                 continue;
45             }
46             if ( isp(ch1) < icp(ch) )///算法步骤 2)的情况 ② 的第一种
47             {
48                 css1.push(ch);///将当前扫描到的操作符 ch 压入操作符栈
49                 i --;
50             }
51             else if (isp(ch1) >= icp(ch))///算法步骤 2)的情况 ② 的第三种
52             {
53                 getTop(a);///获取操作数栈栈顶元素
54                 pop();///弹栈,以便获取第二个元素
55                 getTop(b);///获取操作数栈栈顶元素
56                 pop();///弹栈,已计算过不用再保留
57                 push( doOperator( a, b, ch1 ) );///将计算结果压入运算数栈中
58                 css1.pop();///退栈,已使用过不再保留
59             }
60         }
61     }
62 
63     if (cnt)///细节处理:防止以数字为结尾的字符串,比方说 #1#,没有这个判断的话,输入 #1# 时就会出错:因为1没有被压入操作数栈中
64     {
65         push(temp);
66     }
67      /*
68     #1#
69     #1+1#
70     #2*2+3#
71     #(1)#
72     写代码的时候就是按照让这4个表达式都能正常运行的思路来写的
73     */
74 }

4. 说明及其他

       void SeqStack::calculate( char* Str ) 这个方法我原来放置的参数是两个:charSeqStack& css1, charSeqStack& css2 , css1 是运算符栈,css2 是表达式栈。后面发现这么实现的话只能处理一位数的四则运算。经过考量,决定用C语言型字符串代替作为参数。

  被调用函数在获取C语言型数组与字符串时无法得知其长度,因此第一个while循环作用是确定表达式字符串的长度。第二个while循环从右向左扫描字符串,对每个ch判断后按算法进行相应的操作。

  值得一提是,每扫描到一个数字,就通过表达式 temp = temp + pow( 10, cnt ) * int( ch - ‘0”); 将当前连续数字所表达的确切十进制值计算出来,并保存在临时变量temp中。另外,变量cnt的作用是表示当前有几个连续的数字。当数字连续终止时(如图1所示),将计算结果temp压入调用对象ss1的栈(即操作数栈)中。

  最后展示一下程序的运行截图并简要说明:

  控制台第一行打印的数值为使用形如以下方式得到的结果:

  cout << 200+500*(200+300)*600/709-400 << endl;

  即第一个待求解表达式由C++表达式计算所得结果,以用于与实现得出的结果作比较。

  第1次测试:

  第一个待求解表达式实现得出的结果比由C++表达式计算的结果大1,错误。

  第2次测试:

  第一个待求解表达式实现得出的结果与由C++表达式计算的结果完全一致;

  第4次测试: 第一个待求解表达式实现得出的结果比由C++表达式计算的结果大1582,错误。

  综上所述,实现用于计算一些表达式是正确可行的,而对于另外一些表达式则正确得出结果。另,由实现计算5/3*9与(5/3)*9的结果知:是否添加括号对实现能否正确计算表达式有直接关系;对于不能正确计算的表达式,不同编译器生成的可执行文件得到的结果也不同(如图7与图8所示)。 “栈的应用:后缀表达式求值”算法的实现还是有bug。

图1 表达式字符串中连续数字示意

图3 求解111+56*(789+29)*5/80-400与12+5*(2+3)*6/2-4程序运行截图


图4 求解222+555*(777+111)*666/888-999/333+444+(34%7)与(5/3)*9程序运行截图

 

图5 求解222+555*(777+111)*666/888-999/333+444+(347)与(5/3)*9程序运行截图



图6 计算222+555*(777+111)*666/888-999/333+444+(347)与9%3程序运行截图

 

 

 图7 Code::Blocks 17.12 编译的可执行文件计算200+500*(200+300)*600/709-400程序运行截图


图8 VC6.0 编译的可执行文件计算200+500*(200+300)*600/709-400程序运行截图


图9 计算12+5*(2+3)*6/2-4时操作数栈与操作符栈的变换情况

猜你喜欢

转载自www.cnblogs.com/25th-engineer/p/9902776.html