資料結構之樹與二叉樹(下)
上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過先序遍歷二叉樹的方式求出表示式的值。
一、二叉樹如何表示四則運算
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
附件下載
參考資料
(1)陳廣,《資料結構(C#語言描述)》
(3)zhx6044,《棧和二叉樹的使用》
(4)zero516cn,《算術表示式—二叉樹》
作者:周旭龍
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。