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這樣的詞彙。好在四則運算不是很複雜其處理過程非常簡單:
<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
- 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();
- }
因爲沒有系統學過編譯原理所以這個解決方案不見得高效和優美,也許看完龍書後會在寫一個正規解法版本。