demo下载,示例代码如有bug请通知我并附带您的用例。
在csdn论坛里经常有朋友问动态执行一个四则运算字符串的问题——类似于动态语言的eval执行字符串的功能,因为上学时就没怎么学编译原理所以这类问题一直不会回答。前天群里又有朋友问这个问题由于闲极无聊(本人正在北京求职中,.net高级开发、架构设计方向)就自己瞎琢磨来尝试解决这个问题,也算是在买龙书第二版前的一个练习吧。以下是四则运算的几种形式:
1) 基本的:4 + 3 * 2 / 1 - 7
2) 带括号:(4 + 3) * 2 / (1 - 7)
3) 带正负数:-4 + 3 * 2 / (+1 - 7)
4) 括号中只有一个数字:(4) + (-3) * 2 / 1 – 7
我们手动计算四则运算时会根据括号和运算符优先级来进行判断,这不是一个顺序的过程而是有目的的进行选择安排前后运算执行顺序。如果用程序来做这件事情我们首先要到找到一个合适的数据结构来重新组织四则运算的式子,我第一个能想到的就是树形结构。节点是运算符而叶子就是数字,以(4 + 3) * 2 / (1 - 7)为例的树形结构表示为:
根据这个树形结构我们可以确定2个对象:操作符Op和操作数Num,其关系是个典型的组合模式:
下面要解决的问题就是运算符优先级问题(暂不考虑括号问题),所谓优先级问题也就是如何处理4 + 3 * 2这个形式,亦即怎么提取出树形结构的问题。因为四则运算的运算符只涉及两操作数所以定义一个辅助类来进行运算符优先级的判断。如果3个数和2个运算符都赋值了就可以进行优先级判断并能生成出一个子节点树。其类简要定义如下:
- class ParseAssist
- {
- INode _n1; //操作数1
- INode _n2; //操作数2
- INode _n3; //操作数3
- string _op1 = null; //运算符1
- string _op2 = null; //运算符2
- public void AddNum(INode n) { … }
- public void AddOp(string op) { … }
- //运算符优先级判断
- private bool Priority(string op1, string op2) { … }
- //得到Op对象
- private INode GetOp(string op, INode a, INode b) { … }
- //组合出一个节点树
- public INode Combinate()
- {
- INode tmp = null;
- //判断优先级
- if (this.Priority(this._op1, this._op2))
- {
- //计算前二个数字
- tmp = GetOp(this._op1, this._n1, this._n2);
- this._n1 = tmp;
- this._n2 = this._n3;
- this._op1 = this._op2;
- }
- else
- {
- //计算后两个数字
- tmp = GetOp(this._op2, this._n2, this._n3);
- this._n2 = tmp;
- this._n3 = null;
- this._op2 = null;
- this.CanCompute = false;
- }
- return tmp;
- }
- }
这里只说明一下Combinate方法,一个节点树实际上就是等同于一个Num,并且为了后续计算的需要,需要将生成出来的节点树再赋值回去。举个例子进行说明,如4 + 3 * 2 / 1,程序会先对4 + 3 * 2进行Combinate然后将3 * 2组合为一个节点树,然后加入 /1继续计算。此时_n2 = 3 * 2的节点书对象, _op2 = “/”,_n3 = 1。因此我们就能对整个表达式进行反复的处理,最终形成一颗完整的节点树。而括号部分就相当于一个子表达式只要运用递归就可以简单解决了。不过在做最后一步之前我们要先对表达式进行处理也就是提取出我们关心的词汇,比如+,(,4.5这样的词汇。好在四则运算不是很复杂其处理过程非常简单:
- List<string> wordTree = new List<string>();
- string tmp = "";
- //得到数字
- Action addNum = () =>
- {
- if (tmp.Length > 0)
- {
- wordTree.Add(tmp);
- tmp = "";
- }
- };
- //词法解析
- foreach (var c in expr)
- {
- if (c == ' ')
- {
- addNum();
- continue;
- }
- else if (c == '(')
- {
- addNum();
- wordTree.Add(LEFT_BRACKET);
- }
- else if (c == '+')
- {
- addNum();
- wordTree.Add(PLUS);
- }
- //以下省略
- else
- tmp += c;
- }
- addNum();
因为数字是多个字符的比如1.001,所以需要根据空格、括号和操作符作为边界进行确定。
好了现在万事俱备只需要对词汇列表递归的应用有优先级判断策略就可以最终解决这个问题了。
- //因为要递归运行所以要使用ref来修改词汇列表的当前位置
- private INode Parse(List<string> wordTree, ref int pos)
- {
- var assist = new ParseAssist();
- //计算中间结果
- Action priority_compute = () =>
- {
- //判断是否达到3数字,2操作符
- if (assist.CanCompute)
- assist.Combinate();
- };
- for (; pos < wordTree.Count; ++pos, priority_compute())
- {
- var word = wordTree[pos];
- //遇到左边的小括号进入递归
- if (word == LEFT_BRACKET)
- {
- ++pos;
- assist.AddNum(Parse(wordTree, ref pos));
- continue;
- }
- //遇到右边的小括号推出递归
- if (word == RIGHT_BRACKET)
- break;
- if (this.IsOp(word))
- assist.AddOp(word);
- else
- assist.AddNum(word);
- }
- return assist.Combinate();
- }
因为没有系统学过编译原理所以这个解决方案不见得高效和优美,也许看完龙书后会在写一个正规解法版本。