四则运算 --- 计算器(原创)
本文将从头实现一个支持"加、减、乘、除、小括号"的四则运算器,语言用大众化的js,当然并不会使用动态语言的捷径(eval等)。
-----------------------------------------------------------------------------------------------------------
现给出一个四则运算式:exp = 1 + 2 * ((3+9) * 5 - 7) /3
-----------------------------------------------------------------------------------------------------------
此类问题的常规做法是,首先进行词法分析,然后进行语法分析,最后得出结果。四则运算的因子有普通值、+操作符、-操作符、*操作符、/操作符、小括号6种。 其中除了普通值,其他因子都有“规则能力”,小括号的优先级最高,*/次之,+-最后。
-----------------------------------------------------------------------------------------------------------
我们首先来分析一个简单的式子: 1 + 2
这个式子只有3个因子,1、+、2,如果构建语法树对象,则只有这三个节点,其中+节点具有运算能力,1,2分别为其左右值,建好语法树后,只需要遍历其节点,遇到操作符节点则取左右节点进行运算,其结果就为运算的值。
-----------------------------------------------------------------------------------------------------------
接下来,我们改式子为:1+2+4
这个式子比上一个多了2个因子,如果接着上一个例子的结果继续,那么第一个+号运算后(左右值分别为1,2)得出3,我们知道当遇到下一个+的时候,左右值应该分别为3(1+2得出的值),4,但语法树中第二个+号的左值还是2,所以,当我们应该把左右值得出的结果付给右值,从而使后续运算可以继续。 这也是从左到右运算的基本原理。
-----------------------------------------------------------------------------------------------------------
上述情况属于只有+-运算的时候,现在我们加上乘除。
改式子为 1 + 2 + 4 * 6
构建语法树的话 ,就会有节点如下:1、+、2、+、4、*、6,这7个节点,我们知道乘除的优先级高于加减,所以我们遍历节点,先寻找乘除节点,就会找到*节点,他的左值分别为4,6,进行运算然后赋给右值,此时左值和*操作符已经没用了,但是我们还要遍历此树进行+-运算,所以要把这2个节点剔除。之后语法树变为:1、+、2、+、24,这样就变成了最出的问题。
-----------------------------------------------------------------------------------------------------------
回顾上面几种情况,有6个对象需要创建,分别是:
1 . 语法树对象
2 . +操作符对象
3 . -操作符对象
4 . *操作符对象
5 . /操作符对象
6. 普通值对象
相应代码为:
View Code
var Expression = {};
//加法运算符对象
Expression.Addition =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Addition.prototype = {
get : function(){
var me =this;
return me.left + me.right;
},
toString : function(){
return"+"
}
};
//减法运算符对象
Expression.Subtract =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Subtract.prototype = {
get : function(){
var me =this;
return me.left - me.right;
},
toString : function(){
return"-"
}
};
//乘法运算符对象
Expression.Multiply =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Multiply.prototype = {
get : function(){
var me =this;
return me.left * me.right;
},
toString : function(){
return"*"
}
};
//除法运算符对象
Expression.Divide =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Divide.prototype = {
get : function(){
var me =this;
if(me.right==0)throw Error("被除数不能为0!");
return me.left / me.right;
},
toString : function(){
return"/"
}
};
//值节点
//维护一个数值
Expression.Number =function(num){
var me =this;
me.num = (num==undefined) ?"" : num;
};
Expression.Number.prototype = {
add : function(chr){
var me =this;
me.num += chr;
},
toString : function(){
return parseFloat(this.num);
}
};
//表达式树
//每一个被括号括起来的都表示为一个新的表达式树
//原始式子最外层默认有一个括号
//每个表达式树由无数节点组成,此节点可以是值节点或操作符节点或tree
Expression.Tree =function(){
var me =this;
me.nodes = [];
};
Expression.Tree.prototype = {
add : function(node){
var me =this;
node.parent = me;
me.nodes.push(node);
}
};
现在考虑小括号的问题,该式子为:1+2+(3+6)*7
如果我们把小括号和小括号里的东西假设为x,那么式子变为1+2+x*7,而x最终结果为一个普通值,所以只要我们算出了x,那么问题就会简化,现在我们来把式子改一下,改成(1+2+(3+6)*7),没错,我们在原式子外边加了一个小括号,这对运算或者语意都没有影响。我们发现(3+6)和(1+2+x*7)是同样的表达,那么我们把(1+2+x*7)解析为一颗语法树,同样也可以把(3+6)解析为一颗语法树,这颗语法树附属于上一层语法树。
于是,我们得出了一个结论,所有小括号里的式子我们都解析为语法树,这个语法树依附于上一层小括号所构建的语法树。好,到目前为止,我么已经可以准确的解析一个复杂的四则运算式子。解析器如下:
View Code
//解析器
//解析原始表达式为表达式树
Expression.Parse =function(expStr){
//初始化最初的表达式树
var me =this;
var kv = {"+":me.Addition,"-":me.Subtract,"*":me.Multiply,"/":me.Divide};
var currTree =new me.Tree() , currNum , tree;
for(var i=0;i<expStr.length;i++){
var chr = expStr[i];
//log(chr);
//如果是空白字符跳过
if(chr==""){
continue;
}
//开始构造新的node
elseif("+-*/".indexOf(chr)>-1){
//将当前值节点添加到tree,并指空
if(currNum!=null){
currTree.add(currNum);
currNum =null;
}
//将此操作符对象添加到当前tree
currTree.add(new kv[chr]());
}
//开始构造新的tree
elseif(chr=="("){
tree =new me.Tree();
currTree.add(tree);
currTree = tree;
}
//关闭当前tree
elseif(chr==")"){
//将当前值节点添加到tree,并指空
if(currNum!=null){
currTree.add(currNum);
currNum =null;
}
//当前tree结束,指向父tree
currTree = currTree.parent;
}
else{
if(currNum==null){
currNum =new me.Number();
}
currNum.add(chr);
}
}
if(currNum!=null)currTree.add(currNum);
return currTree;
};
语法树有了,最后就是求值了。每一颗语法树都可能有4中因子:普通值、加减运算符、乘除运算符、子语法树。其中字语法树优先级最高,我们先求出所有子语法树并最终返回一个普通值来代替它在原语法树上的地位。然后是乘除运算符,最后是加减,不再累赘。代码如下:
View Code
Expression.Eval =function(tree){
var me =this,node;
//先计算()
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if(node instanceof me.Tree){
tree.nodes[i] = me.Eval(node);
}
}
//计算*/
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if( (node instanceof me.Multiply) || (node instanceof me.Divide) ){
node.left = tree.nodes[i-1];
node.right = tree.nodes[i+1];
//将得到的结果包装为Number替代tree[i+1],并将node和tree[i-1]删除且重定向i位置
tree.nodes[i+1] =new me.Number(node.get());
tree.nodes.splice(i-1,2);
i-=2;
}
}
//计算+-
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if(!(node instanceof me.Number)){
if(tree.nodes[i-1])node.left = tree.nodes[i-1];
node.right = tree.nodes[i+1];
//将得到的结果包装为Number替代tree[i+1],并将node和tree[i-1]指空
tree.nodes[i+1] =new me.Number(node.get());
}
}
return tree.nodes[tree.nodes.length-1];
};
到此为止,我们已经成功得构建了一个完整的四则运算器,按照化繁为简的方法论,我们把一个复杂的式子最终转变为最基本的式子,从而求出答案,最后我们对文章开头的式子exp进行解析运算,得到的语法树和答案如下:
1.6
+
2
*
==========3
==========+
==========9
=====*
=====5
=====-
=====7
/
3
要解析的式子为 : 1.6+2* ((3+9) *5-7) /3
结果为:36.93333333333334
最后附上所有代码:
View Code
var log =function(s){
document.write(s+"<br/>");
};
var Expression = {};
//加法运算符对象
Expression.Addition =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Addition.prototype = {
get : function(){
var me =this;
return me.left + me.right;
},
toString : function(){
return"+"
}
};
//减法运算符对象
Expression.Subtract =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Subtract.prototype = {
get : function(){
var me =this;
return me.left - me.right;
},
toString : function(){
return"-"
}
};
//乘法运算符对象
Expression.Multiply =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Multiply.prototype = {
get : function(){
var me =this;
return me.left * me.right;
},
toString : function(){
return"*"
}
};
//除法运算符对象
Expression.Divide =function(){
var me =this;
me.left =0;
me.right =0;
};
Expression.Divide.prototype = {
get : function(){
var me =this;
if(me.right==0)throw Error("被除数不能为0!");
return me.left / me.right;
},
toString : function(){
return"/"
}
};
//值节点
//维护一个数值
Expression.Number =function(num){
var me =this;
me.num = (num==undefined) ?"" : num;
};
Expression.Number.prototype = {
add : function(chr){
var me =this;
me.num += chr;
},
toString : function(){
return parseFloat(this.num);
}
};
//表达式树
//每一个被括号括起来的都表示为一个新的表达式树
//原始式子最外层默认有一个括号
//每个表达式树由无数节点组成,此节点可以是值节点或操作符节点或tree
Expression.Tree =function(){
var me =this;
me.nodes = [];
};
Expression.Tree.prototype = {
add : function(node){
var me =this;
node.parent = me;
me.nodes.push(node);
}
};
//解析器
//解析原始表达式为表达式树
Expression.Parse =function(expStr){
//初始化最初的表达式树
var me =this;
var kv = {"+":me.Addition,"-":me.Subtract,"*":me.Multiply,"/":me.Divide};
var currTree =new me.Tree() , currNum , tree;
for(var i=0;i<expStr.length;i++){
var chr = expStr[i];
//log(chr);
//如果是空白字符跳过
if(chr==""){
continue;
}
//开始构造新的node
elseif("+-*/".indexOf(chr)>-1){
//将当前值节点添加到tree,并指空
if(currNum!=null){
currTree.add(currNum);
currNum =null;
}
//将此操作符对象添加到当前tree
currTree.add(new kv[chr]());
}
//开始构造新的tree
elseif(chr=="("){
tree =new me.Tree();
currTree.add(tree);
currTree = tree;
}
//关闭当前tree
elseif(chr==")"){
//将当前值节点添加到tree,并指空
if(currNum!=null){
currTree.add(currNum);
currNum =null;
}
//当前tree结束,指向父tree
currTree = currTree.parent;
}
else{
if(currNum==null){
currNum =new me.Number();
}
currNum.add(chr);
}
}
if(currNum!=null)currTree.add(currNum);
me.PrintTree(currTree);
log("要解析的式子为 : "+ expStr);
log("结果为:"+ me.Eval(currTree));
};
Expression.Eval =function(tree){
var me =this,node;
//先计算()
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if(node instanceof me.Tree){
tree.nodes[i] = me.Eval(node);
}
}
//计算*/
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if( (node instanceof me.Multiply) || (node instanceof me.Divide) ){
node.left = tree.nodes[i-1];
node.right = tree.nodes[i+1];
//将得到的结果包装为Number替代tree[i+1],并将node和tree[i-1]删除且重定向i位置
tree.nodes[i+1] =new me.Number(node.get());
tree.nodes.splice(i-1,2);
i-=2;
}
}
//计算+-
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if(!(node instanceof me.Number)){
if(tree.nodes[i-1])node.left = tree.nodes[i-1];
node.right = tree.nodes[i+1];
//将得到的结果包装为Number替代tree[i+1],并将node和tree[i-1]指空
tree.nodes[i+1] =new me.Number(node.get());
}
}
return tree.nodes[tree.nodes.length-1];
};
Expression.PrintTree =function(tree,spaces){
var me =this,node;
spaces = spaces ||"";
for(var i=0;i<tree.nodes.length;i++){
node = tree.nodes[i];
if(node instanceof me.Tree){
me.PrintTree(node,spaces+"=====");
}else{
log(spaces+node);
}
}
};
感兴趣的同学,可以直接copy上述代码进行研究。
----------------------------------------------------------------------------------------------------------
补充:
在四则运算中,+-还有其二义性,除了能表示操作符,还能表示数值的正负,比如+2,-3。一般来说,+2中的+是不必要的,在四则运算中好像也不该这么写(我忘记了,但并不影响),所以我们就只考虑-3这种情况。 为了方便解析,我们把-3看成
0-3,于是他又成了一个表达式,有左右值,有操作符。 现在我们考虑-3会出现在什么地方,没错,它只能出现在表达式树的最左端,也就是左小括号"("的右边,那么在我们遍历表达式树节点的时候,我们遇到这个减号,由于它处在表达式树的最左端,他将没有左值,那么我们只需要判断是否有左节点就行了,如果没有我们就不重置操作符的左值(操作符的左值默认为0),这样就ok了。

