上面兩篇我們了解了樹的基本概念以及二叉樹的遍歷算法,還對二叉查找樹進行了模擬實現。數學表達式求值是程序設計語言編譯中的一個基本問題,表達式求值是棧應用的一個典型案例,表達式分為前綴、中綴和后綴三種形式。這里,我們通過一個四則運算的應用場景,借助二叉樹來幫助求解表達式的值。首先,將表達式轉換為二叉樹,然后通過先序遍歷二叉樹的方式求出表達式的值。
一、二叉樹如何表示四則運算
1.1 表達式轉換為二叉樹
上圖是表達式“3+2*9-16/4”轉換成的二叉樹,觀察表達式,可以看出:
(1)操作數都是葉子節點;
(2)運算符都是內部節點;
(3)優先運算的操作符都在樹下方,而相對優先級較低的減法(根節點)運算則最后運算。
從上往下看,這棵二叉樹可以理解如下:
(1)要理解根節點"-"號的結果必須先計算出左子樹"+"和右子樹"/"號的結果。可以看,要想得到"+"號的結果,又必須先計算其右子樹"*"號的結果;
(2)"*"號左右孩子是數字,可以直接計算,2*9=18。接下來計算"+"號,3+18=21,即根節點的左子樹結果為21;
(3)"/"號左右孩子是數字,可以直接計算,16/4=4。於是,根節點的右子樹結果為4。
(4)最后計算根節點的"-"號,21-4=17,於是得出了該表達式的值為17。
1.2 二叉表達式樹的構造過程解析
從上面的解析過程可以看出,這是一個遞歸的過程,正好可以用二叉樹先序遍歷的方法進行計算。下面我們來一步一步地通過圖示來演示一下表達式"3+2*9-16/4"解析生成二叉樹的過程。
(1)首先獲取表達式的第一個字符“3”,由於表達式樹目前還是一棵空樹,所以3成為根節點;
(2)獲取第二個字符“+”,此時表達式樹根節點為數字,需要將新節點作為根節點,原根節點作為新根節點的左孩子。這里需要注意的是:只有第二個節點會出現這樣的可能,因為之后的根節點必定為操作符;
(3)獲取第三個字符“2”,數字將沿着根節點右鏈插入到最右端;
(4)獲取第四個字符“*”,如果判斷到是操作符,則將與根節點比較優先級,如果新節點的優先級高則插入成為根節點的右孩子,而原根節點的右孩子則成為新節點的左子樹;
(5)獲取第五個字符“9”,數字將沿着根節點右鏈插入到最右端;
(6)獲取第六個字符“-”,“-”與根節點“+”比較運算符的優先級,優先級相等則新節點成為根節點,原表達式樹則成為新節點的左子樹;
(7)獲取第7與第8個字符組合為數字16,沿着根節點右鏈插入到最右端;
(8)獲取第九個字符“/”,與根節點比較運算符的優先級,優先級高則成為根節點的右孩子,原根節點右子樹則成為新節點的左子樹;
(9)獲取第十個字符“4”,還是沿着根節點右鏈查到最右端。至此,運算表達式已全部遍歷,一棵表達式樹就已經建立完成。
SUMMARY:從以上過程中我們可以將表達式樹的建立算法歸結如下
①第一個節點先成為表達式樹的根;
②第二個節點插入時變為根節點,原根節點變為新節點的左孩子;
③插入節點為數字時,沿着根節點右鏈插入到最右端;
④插入節點為操作符時,先跟根節點操作符進行對比,分兩種情況進行處理:
一是當優先級不高時,新節點成為根節點,原表達式樹成為新節點的左子樹;【如上面的步驟(6)】
二是當優先級較高時,新節點成為根節點右孩子,原根節點右子樹成為新節點的左子樹。【如上面的步驟(8)】
二、二叉表達式樹的模擬實現
2.1 二叉表達式樹節點的定義

private class Node { private bool _isOptr; public bool IsOptr { get { return _isOptr; } set { _isOptr = value; } } private int _data; public int Data { get { return _data; } set { _data = value; } } private Node _left; public Node Left { get { return _left; } set { _left = value; } } private Node _right; public Node Right { get { return _right; } set { _right = value; } } public Node(int data) { this._data = data; this._isOptr = false; } public Node(char optr) { this._isOptr = true; this._data = optr; } public override string ToString() { if (this._isOptr) { return Convert.ToString((char)this._data); } else { return this._data.ToString(); } } }
與普通二叉樹節點定義不同,這里新增了一個isOptr標志,來判斷該節點是數字節點還是運算符節點;
2.2 二叉表達式樹的創建實現
private Node CreateTree() { Node head = null; while(_pos < _expression.Length) { Node node = GetNode(); // 將當前解析字符轉換為節點 if(head == null) { head = node; } else if (head.IsOptr == false) // 根節點為數字,當前節點為根,原根節點變為左孩子 { node.Left = head; head = node; } else if (node.IsOptr == false) // 如果當前節點是數字 { // 當前節點沿右路插入最右邊成為右孩子 Node tempNode = head; while(tempNode.Right != null) { tempNode = tempNode.Right; } tempNode.Right = node; } else // 如果當前節點是運算符 { if (GetPriority((char)node.Data) <= GetPriority((char)head.Data)) // 優先級低則成為根,原二叉樹成為插入節點的左子樹 { node.Left = head; head = node; } else // 優先級高則成為根節點的右子樹,原右子樹成為插入節點的左子樹 { node.Left = head.Right; head.Right = node; } } } return head; }
這里按照我們在上面所歸納的創建過程算法進行了實現,代碼中的注釋已經比較完善,這里就不再贅述。
2.3 二叉表達式的先序遍歷計算運算結果實現
// 先序遍歷進行表達式求值 private int PreOrderCalc(Node node) { int num1, num2; if (node.IsOptr) { // 遞歸先序遍歷計算num1 num1 = PreOrderCalc(node.Left); // 遞歸先序遍歷計算num2 num2 = PreOrderCalc(node.Right); char optr = (char)node.Data; switch (optr) { case '+': node.Data = num1 + num2; break; case '-': node.Data = num1 - num2; break; case '*': node.Data = num1 * num2; break; case '/': if (num2 == 0) { throw new DivideByZeroException("除數不能為0!"); } node.Data = num1 / num2; break; } } return node.Data; }
這里通過遞歸地進行先序遍歷,也就是求得根節點(運算符)的兩個子樹的值,最后再通過對這兩個值進行根節點運算符的計算得到最終的結果。
2.4 四則運算運行結果
由於本表達式樹的設計較為簡單,沒有考慮到帶括號的情形,因此這里只用不帶括號的表達式進行查看,運行結果如下圖所示:
(1)3+2*9-16/4
(2)4*5-16/4+2*9
附件下載
本文所實現的二叉表達樹求解四則運算的C#代碼:http://pan.baidu.com/s/1eQheNQy
參考資料
(1)陳廣,《數據結構(C#語言描述)》
(2)隱約有歌,《C#實例講解二叉樹原理與實現》
(3)zhx6044,《棧和二叉樹的使用》
(4)zero516cn,《算術表達式—二叉樹》