最近我修改了我以前写的数学表达式计算器。以前那个是用最基本的优先级队列实现的,如果只是进行少量的式子计算,效率还勉强可以。但是,如果用于函数的作图,效率真的不敢恭维。
我先讲一下以前是怎么用优先级队列实现的。
1、接受用户直接输入的算式。如: sin(cos(tan(log[45.4^3-3]&(Pi*3.4)))/3.4*e*c*3.4E4)
2、对这个算式进行词法分析。
3、语法分析。首先分析算式中有可能出现的书写错误。然后创建数据数组,用于记录数值,如:45.4、3.4等。最后,把每一个函数单元(sin、cos等)用一个结构Datamark包装。
struct DataMark
{
int m_headmark; //操作数在数据数组中的位置
int m_tailmark; //操作数在数据数组中的位置
DataOperator m_name; //记录操作符的类别
DataQueue *m_childqueue; //用于处理括号的情况。如果这个节点是括号,则这个是保存这个括//号里面子算式的优先级队列。
PowerValue m_power; //运算符的优先级
DataMark() //构造函数
{
m_headmark = 0;
m_tailmark = 0;
m_childqueue = NULL;
m_name = O_NONETHISOPERATOR;
};
~DataMark() //析构函数
{
};
};
其中这个结构记录相关的优先级、函数名字(枚举)、操作数、每一个操作数数值在对应的数据数组中的下标。
4、把“函数单元”结构传入队列。
5、从优先级队列中按顺序弹出运算级最高的结构,分析函数类型,然后调用相应的自定义类成员函数进行计算。把每一步计算的结果传回数值数组并覆盖原来位置的值。单是把数据传回数据数组这一步复杂度就为O(n^2),其中n是数值的数量。
6、返回数组中第一个元素的值。该值就是算式的值。
明显,以前的方法虽然直观,但是对于要对同一个算式计算多次的情况(如函数作图器)就做了很多重复低效的工作。
所以,我从树结构中收到启发,从新写了一个计算器。例如,我们要计算这样一个算式:sin(4.3*Pi) – (4 *4 – 4 ) 在树结构是这样储存的:
Minus
sin
Minus
Multiply
4.3
Pi
Multiply
4
4
4
Minus
sin
Minus
Multiply
4.3
Pi
Multiply
4
4
4
Minus
sin
Minus
Multiply
4.3
Pi
Multiply
4
4
4
![](/images/load.gif)
其中一些节点是储存操作的(具体是函数地址),如Multiply;而另一些如4就是储存数据的。具体过程是利用二叉树遍历的方法,从树的左下角元素开始,采用深度优先遍历树,同时遇到数值,并且如果其兄弟节点也是数据节点的话,就利用父节点A所储存的函数地址调用相关的函数计算,然后把结果保存在节点A中,这时候A就变成是数据节点,然后继续搜索下一个节点,采取同样的办法处理。就这样,当最后遍历回根节点就出现两种情况:1、根节点是函数节点:调用函数计算两个(或者一个)孩子的数据,最后返回。
2、根节点是数据节点:直接返回数值。
这里就出现一个问题,就是怎么建立这样的树呢。我是采用栈来实现的。具体和单纯用栈做中缀表达式求值很相似(本文后有简单说明),只是把从操作符栈弹出运算符那步骤换成是:先从操作符栈弹出运算符;然后从数据栈中弹出两个或一个数据(具体由操作符的结合数决定),并把它们连接到运算符节点中;最后再把这个运算符压到数据栈中,把它当作数据处理。 就这样,知道所有元素均成为树节点后,建立树的工作完成。
所以很明显,对于同一条式子来说,用上面的方法计算含有未知数X的函数式只需要作一次算式分析,对于每一个X只需要修改相应的节点,然后从新遍历一次树就行了。其实关键还是树这种结构能够反映数据计算的先后关系。
==================================
现在我引用书上的说法对用栈求中缀表达式的算法作简单说明。
中缀表达式求值算法用操作数栈(浮点数栈)来存放操作数和中间运算结果,另一个运算符栈存放运算符和可以使我们实现优先级的左括号,读入表达式是,将表达式元素压入各自的栈中。即遇到操作数时,将其压入操作数栈,而当运算需要时,将其从栈中弹出,而仅当当前栈中优先级大于或者等于当前的运算符优先级,才将运算符弹出,当执行完后才将运算符从栈中弹出,即当后续输入的运算符优先级小于或等于他的优先级或表达式结束时才弹出。运算符优先级分两次给定,第一次是读入时(称为读入优先级),第二次时压入栈后(称为栈优先级)。读入优先级和栈优先级比较,用于括号和右结合运算符。(我就讲这么多了,具体看数据结构吧)