分享Delphi实现数学表达式的计算(逆波兰式法)-四则运算解析

tjs_125 2012-05-14 08:49:52
加精
http://www.cnblogs.com/tangqs/archive/2011/11/03/2234715.html
众所周知,计算机处理表达式的难点在于括号的处理,在通常的表达式中,二元运算符总是置于与之相关的两个运算对象之间,所以,这种表示法也称为中缀表示。波兰逻辑学家J.Lukasiewicz于1929年提出了另一种表示表达式的方法。按此方法,每一运算符都置于其运算对象之后,故称为后缀表示。如a+b表达为ab+。这种后缀表达式非常方便去掉运算符优先级的影响与括号,甚至是单目运算符:
示例说明:
1. a*b+c*(d+e) 逆波兰表达式: ab*cde+*+
2. -a+-b*+c 逆波兰表达式: a~b~c@*+
3.a-(-(b-c)) 逆波兰表达式: abc-~ -
预处理:
-(负号)处理:用~代替
+(正号)处理:用@代替,或者将其在字符串中删除
数学自然常数 e 与圆周率 pi 的用 (2.718281828459045) 与 (3.1415926535897932384) 代替(包含前后的括弧)

那么计算机怎样通过后缀式来进行运算呢?这里首先假设读取分析表达式的准备工作都已经做好了,那么首先需要做的是把表达式转换成后缀式,也就是逆波兰表达式的构建过程。
构建器由两个主要组件组成,一个是目标表达式的存储器,另一个是一个符号栈。与源表达式的扫描顺序一样,存储器是从左向右储存数据的,而符号栈则遵守后进先出的原则:
* 读入一个数据(数值与函数名非单个字符,需要做判断处理)
1. 如果是左单目运算符或者函数名,直接入符号栈;比如 正负号 ~ @ max sin
2. 如果是右单目运算符,直接入存储器栈;比如 阶乘!与百分号%
3. 如果是运输量,则直接写入存储器;检查符号栈顶是否有单目运算符,有的话则全部出栈,并写入存储器;
4. 如果是左括号"(",则直接入符号栈;
5. 如果是右括号")",则弹出符号栈数据,写入存储器,一直到左括号弹出,再检查栈顶是否为左单目运算符或者函数名,是的话继续弹出,直到遇到双目运算符;
6. 如果是普通运算符,则与栈顶符号比较优先级,若大于栈顶优先级,则入栈;否则弹出栈顶符号并写入存储器,直到栈顶符号的运算优先级较小为止;
7.如果是函数参数的连接逗号“,”时,则弹出符号栈数据,直到遇到左括弧 ( 或者逗号,为止,再将逗号,入符号栈;
8.如果是结束符(表示表达式已全部读完),则符号栈全部弹出并写入存储器,否则读取数据进入下个过程;
此外还有一些处理的技巧,比如定义一个优先级最低的运算符作为表达式结束的标志(用#标示,添加在表达式结尾处),在符号栈里首先加入一个结束标志,那么表达式读完时则自动弹出栈中所有符号,并写入存储器结尾表示成功。
接下来是计算的过程。计算的时候除了刚才构建的数据外,还需要另外一个计算中间值存储栈。
1、首先是从左至右扫描数据段,如果读出的是数据则压入计算中间值存储栈;
2、遇到单目运算符号就从计算中间值存储栈弹出一个数据进行运算,再把结果压回计算中间值存储栈;
3、遇到双目运算符号就从计算中间值存储栈弹出两个数据进行运算,再把结果压回计算中间值存储栈;这里需要注意减法与除法的计算顺序是第一次弹出的值作为减数和除数,第二次弹出的值作为被减数和被除数。
4、遇到逗号,就从计算中间值存储栈弹出两个数据用“,”连接起来直接将数值字符串压入计算中间值存储栈,不做计算。比如 12 13 , 压入13,12
5、遇到函数,弹出计算中间值存储栈的相关数据调用函数进行计算;
这样,返回结果就是栈中唯一的数据,我们完成了逆波兰表达式的全部计算过程。
最后还有一点就是检查给点表达式是否正确,就是下面的
function CheckCalcExp(const ExpT: string; var AInfo: string): boolean;
这样保证计算不会出错,具体见代码。
/* 表达式计算 */
/* 调用方式:CalcExp('1+max(0.5,sin(1))+sum(1,2^3,mod(5,3))', res, infoStr) */
/* 带符号参数调用方法,先调用符号定义AddSignParam,再调用 CalcExp: */
/* AddSignParam(['a','s'], [1, 0.5]); 或者 AddSignParam('a=1,s=0.5') */
/* CalcExp('1+a+sin(s)', res, infoStr) */
/* 其中res存储计算结果,为double型;infoStr存储计算时的提示信息,为string */
表达式计算器 V2.3 支持以下功能:
1、四则运算 + - * / 、括弧()、正负(+ -)
2、百分数 %、求幂 ^ 、整数阶乘 ! (1 至 150)
3、参数符号计算,示例:a+b @@a=1,b=2 结算结果为3
用@@表示表达式中定义符号的值
4、常数e、圆周率PI
5、丰富的函数功能:
统计函数: max,min,sum,avg,stddev 标准偏差,均支持多参数
三角函数: sin,cos,tan,arcsin,arccos,arctan
degrad(60) 角度转弧度
raddeg(3.14) 弧度转角度
costh(a,b,c) 余弦定理 cosC)
指数对数函数:sqrt,power(x,y),abs,exp,log2,log10,logN(a,N),ln
数据处理函数:int(x),trunc(x) 取整
frac(x) 取小数部分
round(x) 四舍五入取整
roundto(x,-1) 保留一位小数
mod(M,N) 求模
几何面积函数:s_tria(a,b,c) 三角形面积
s_circ(r) 圆形面积
s_elli(a,b) 椭圆面积
s_rect(a,b) 矩形面积
s_poly(a,n) 正多边形面积
平面几何函数:pdisplanes(x1,y1,x2,y2) 平面两点距离
pdisspace(x1,y1,z1,x2,y2,z2) 空间两点
p_line(x0,y0, A, B, C) 平面点到线距离
p_planes(x0,y0,z0 A, B, C, D)空间点到面距离
数列求和: sn(a1, d, n) 等差数列前n项和
sqn(a1, q, n) 等比数列前n项和
个税计算函数:intax(x), arcintax(x) 个税反算
6 、历史计算记录,双击计算记录可重新修改计算
示例: sin(1)+(-2+(3-4))*20% , e^63+PI , 15! , log2(max(2,3))
注: 运算符必须为半角格式,三角函为弧度,输入可用空格间隔
代码太大,大家可以到下面网站下载:
http://www.cnblogs.com/tangqs/archive/2011/11/03/2234715.html
...全文
5851 48 打赏 收藏 转发到动态 举报
写回复
用AI写文章
48 条回复
切换为时间正序
请发表友善的回复…
发表回复
  • 打赏
  • 举报
回复
楼主你好,我根据你的思路用C#写了个科学计算器,但是有错误,请问能把你的代码发给我对比一下吗?邮箱是648479632@qq.com,多谢!
tjs_125 2013-08-16
  • 打赏
  • 举报
回复
引用 45 楼 Javkburd 的回复:
楼主真赞,帮了我很大的忙,我正在做计算器,在处理单目运算福上遇到一点问题。
引用 46 楼 u011708245 的回复:
谢谢分享终于找到解决了
能帮助到你们我也感觉很高兴! 最近我又把表达式计算代码封装成类,并且增加了扩展功能,可以根据需要再派生出新的类,新类里面可以自己定义自己需要的自定义函数,扩展计算功能,有需要可以留下邮箱。
新青叶 2013-08-15
  • 打赏
  • 举报
回复
谢谢分享终于找到解决了
JackBurd 2012-06-13
  • 打赏
  • 举报
回复
楼主真赞,帮了我很大的忙,我正在做计算器,在处理单目运算福上遇到一点问题。
qingfeng5251 2012-05-23
  • 打赏
  • 举报
回复
谢谢分享
mimacuowuma 2012-05-22
  • 打赏
  • 举报
回复
谢谢分享
cntigercat 2012-05-21
  • 打赏
  • 举报
回复
这个看了真的头晕
zhangdawei1976 2012-05-20
  • 打赏
  • 举报
回复
谢谢分享
blackkettle 2012-05-20
  • 打赏
  • 举报
回复
逆波兰式法, 好东西!
txzsp 2012-05-20
  • 打赏
  • 举报
回复
谢谢分享
LAONINGA098 2012-05-18
  • 打赏
  • 举报
回复
谢谢分享,我也补习一下
windwind120 2012-05-18
  • 打赏
  • 举报
回复
学习学习
-HelloWorld- 2012-05-18
  • 打赏
  • 举报
回复
谢谢分享,果断学习
biololo 2012-05-18
  • 打赏
  • 举报
回复
正负号不需要预处理为其他符号
biololo 2012-05-18
  • 打赏
  • 举报
回复
单目不需要区分左右
biololo 2012-05-18
  • 打赏
  • 举报
回复
非常荣幸可以看到楼主的实现,就一些实现技巧与楼主切磋,不对之处,还望斧正。

计算器的实现

一般这类程序主体分为表达式解析(token),语法分析(parse),运算。这三个部分是可以独立替换的。
表达式的解析 token,一般有通用的程序,也可以手工编写。很多script,例如Lus在开始几个版本中,是作者手工编写解析器的,但后来随着语言的发展转而使用YACC来做。解析是根据定义的规则来分词,在这个层面,表达式中的元素一般被抽象成以下几种类型:数字,字符串常量,符号,标记字符和空格。

数字,指的是语句中允许的数字的表达方式。可能是整数,由0-9组成,整个串的所有字符ASCII范围在48-57。也可能是浮点数,中间会包含小数点,且小数点不在第一和最后一位。也可能是科学计数法,或货币表示法,等其他类型的浮点数表示法。

字符串常量,指的是以引号(根据语法定义,可以为单引号或双引号)开始,以引号结束的串,中间所有的符号都会被忽略,不被解析,除非遇到转义。

符号,语言在设计的时候会预定义符号集,除了表示块(block)的和特殊用途的符号外,其他用以计算的符号可以简单理解为函数的缩写,因为他们在处理上和函数的方式类似。(就我个人感觉,无论是类似C的语言,还是类似Pascal的语言,在语法设计上是有些怪异的,从数学的角度来看,这些语言表达上混合了前缀、中缀和后缀表达式,例如函数,赋值和运算式,和某些数学表达式。所以,我认为,最为统一的表达是Lisp。)

标记符,一般指保留字(reserved word)和函数名。不同语言定义中,会有不同的定义规则,保留字会有一个常量列表;函数命名有约定,例如只能以下划线_或字母开头,名称中字符只能为字母数字和下划线,等。凡是不满足这些定义规则的标记符,可以认为是无效字符。

空格,一般表达式中空格、不可见字符(包括控制字符,例如NUL,TAB,LF,CR,和其他不可见字符)。空格在分词后,全部都被舍弃掉。

其他,指的是不满足上述定义规则的其他字符,这些都被认为是无效字符,当作异常来处理。

表达式解析的结果一般被实现为结构体队列或对象队列,其中每一项存储的是一个token和其类型。

解析表达式,可以选择不同语言实现版本的YACC嵌入到程序中,也可以手工解析。在手工解析时,一个比较清晰的实现方式是使用状态机。由于兼顾性能,这里不必使用通用状态机的实现。

语法分析。将表达式解析的结果(token),根据语义规则组成语法树(syntax tree)。在生成语法树时,一些token会被舍弃掉,例如括号(){}[]逗号,等,被舍弃的token往往是用来标记block或分割token之用,而运算符号不会被丢弃,成为syntax tree的枝节点,叶节点为数据。syntax tree可能会被多次扫描生成,用以消除二义性和优化。在实现诸如计算器,或者简单的脚本语言的时候,使用前缀或后缀表达式来实现syntax tree。(这个转换过程也是很多Lisp拥趸讽刺其他语言的地方,因为一个合乎语法的Lisp写出来就已经是前缀表达式,而不需要再做语法分析的过程了,这也是所谓数据即代码。)

在本示例中,语法树结果大致如下:
+
/\
! Sum
/ /|\
5 1 2 Max
/\
3 4

大学数据结构教材中都有介绍后缀表达式的生成算法。如果不考虑上下文和二义性分析的话,可以将所有的运算符都当成函数来处理。(在实现计算器或者简单的脚本解析时都可按照以下的方式来处理。)在实现这个过程的关键是定义一个正确的优先级列表,在这张优先级列表中,函数也需要被赋予一个优先级,当然,不同函数的优先级是相同的。

例如:5!+Sum(1,2,Max(3,4))
首先扫描表达式,解析出每一项(token):5 ! + Sum ( 1 , 2 , Max ( 3 , 4 ) )
注意,感叹号!在这里被设计成 阶乘,+表示数值相加,Sum和Max为预定义函数,无论是预算符号还是函数,都应该在解析这句语句之前被写入到一个符号表,在解析这句语句时,将token与符号表中的成员对比,来做有效性校验。

将表达式转为后缀式:5 ! 1 , 2 , 3 , 4 Max Sum +
前面提到过,其实在这句例句中,混合了前,中后缀的表达,parse的过程就是消除这种混合的形式,由于其结构特性,表示block或priority的括号()[]{}将被舍弃,这里,无论是针对表示运算优先级的(),还是表示函数参数scope的(),还是表示数组边界的[],甚至是语句块的{},都是相同的处理方式。
构建语法树的一个简单的形式就是生成后缀表达式,在判定优先级时,函数Max与Sum都被视为相同的优先级,该优先级要比逗号,要低。
这样,无论是在原语句中表示为后缀式的阶乘!运算符,还是被表示成前缀式的函数,都被转换成后缀表达式,使用一种的方式来计算。

非常幸运的是,如果我们只是讨论一个计算器的编写,而不是语言的实现,我们并不需要对语法树做多次扫描,不需要对其做语法检查和优化处理。接下来,就可以直接讨论求值了。

严格来说,接下来要讨论的求值运算和compile中的代码生成本质是一样的,就是将statement中的各种符号转变成预定义的函数,而执行这些预定义的函数进行求值,也就相当于执行一个编译好的application了。

如果前一个步骤,我们采用面向对象的设计,将语法树生成为如下形式
+
/\
! Sum
/ /|\
5 1 2 Max
/\
3 4
计算就非常容易,只需要调用root node,递归执行即可。使用syntax tree,在判定函数参数个数,以及处理函数参数默认值时非常容易。

国内绝大部分的数据结构教材中,在介绍逆波兰式运算的时,提到:”当读取到+或-运算符时,就从数据栈中弹出两个数据计算“。这里,从数据栈中弹出几个数据,根本原因是由运算符需要几个参数决定的,如果将运算符映射成函数Add Sub,也就是由Add Sub函数有多少参数决定的,这样来设计程序,在处理+ - 与 处理 Sum Max时,就没有区别了,可以使用一致的逻辑。另外,对于负数 -3,指的是0-3,即sub函数的第一个参数默认值为0,可以不填,当计算后缀式的时候,遇到-,需要从栈顶弹出两个数据项填充Sub的参数(从右往左填参,先填第二个参数,再填第一个参数),如果只弹出一个参数就遇到栈底或括号,则第一个参数就使用默认值0,这样就不要分析上下文来识别-是运算符还是正负号,也就可以不必将-3当成一个整体来对待但仍然不会产生错误。注意,表达式正确的写法应该为2+(-3),而不是2+-3(如果一定需要这种写法,则要将-的优先级设置的比+高)

如果表达式转为后缀式:5 ! 1 , 2 , 3 , 4 Max Sum + 来求值,详细过程在任何一本数据结构的书中都有,但是我想指出的是,第一,对于运算符的,首先根据符号映射的具体实现函数来确定其需要的参数,从数据栈中弹出,如果遇到数据栈中不能弹出足够数量的参数,首先看函数定义中是否有默认值,其次再判定是否是异常。第二,并不是每一个符号都一定要立即求值,例如逗号,在作为函数参数分隔符时,其运算规则可被处理为将token合并为列表,将列表整体压入数据栈,当后面遇到函数需要读参时(需要根据函数的实现来判定,函数是否有参数,函数即便没有参数,在写成中缀式时,也会使用括号()来表示空参数列表,此时在解析时,遇到()需要使用空NULL作为占位符,用以解决函数使用默认参数而没有实参的情况),总是从栈顶弹出第一个数据作为参数。

一般说来,针对后缀表达式计算比针对syntax tree实现上要复杂一些。
另外,以上提到的『预定义函数』,在实现时,可以使用class而不一定是function来实现,这样,通过读取RTTI等技巧,使得结构更好。
tjs_125 2012-05-18
  • 打赏
  • 举报
回复
看了很多回帖说看不懂,我特意写了一篇举例说明的文章,有兴趣的可以去看看,
因文章长,这里的格式又不太支持,可以到下面地址去看看:
文章地址:http://www.cnblogs.com/tangqs/archive/2012/05/18/2507708.html
xiyue8 2012-05-17
  • 打赏
  • 举报
回复
大家都是高手啊~~ 我能回复这么牛X的帖子不
a506498 2012-05-17
  • 打赏
  • 举报
回复
刘明 有时间学习学习,
dzzhlc 2012-05-17
  • 打赏
  • 举报
回复
表情看不懂
加载更多回复(12)

16,748

社区成员

发帖
与我相关
我的任务
社区描述
Delphi 语言基础/算法/系统设计
社区管理员
  • 语言基础/算法/系统设计社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧