1. 程式人生 > >資料結構之樹與二叉樹(下)

資料結構之樹與二叉樹(下)

上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過先序遍歷二叉樹的方式求出表示式的值。

一、二叉樹如何表示四則運算

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,《算術表示式—二叉樹

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。