數據結構基礎溫故-4.樹與二叉樹(中)

上一篇中,咱們瞭解了樹的基本概念以及二叉樹的基本特色和代碼實現,還用遞歸的方式對二叉樹的三種遍歷算法進行了代碼實現。可是,因爲遞歸須要系統堆棧,因此空間消耗要比非遞歸代碼要大不少。並且,若是遞歸深度太大,可能系統撐不住。所以,咱們使用非遞歸(這裏主要是循環,循環方法比遞歸方法快, 由於循環避免了一系列函數調用和返回中所涉及到的參數傳遞和返回值的額外開銷)來從新實現一遍各類遍歷算法,再對二叉樹的另一種特殊的遍歷—層次遍歷進行實現,最後再瞭解一下特殊的二叉樹—二叉查找樹。html

1、遞歸與循環的區別及比較

1.1 遞歸爲什麼很慢?

  你們都知道遞歸的實現是經過調用函數自己,函數調用的時候,每次調用時要作地址保存,參數傳遞等,這是經過一個遞歸工做棧實現的。具體是每次調用函數自己要保存的內容包括:局部變量、形參、調用函數地址、返回值。那麼,若是遞歸調用 N 次,就要分配 N*局部變量、N*形參、N*調用函數地址、N*返回值,這勢必是影響效率的。node

關於系統棧和用戶棧:算法

①系統棧(也叫核心棧、內核棧)是內存中屬於操做系統空間的一塊區域,其主要用途爲: (1)保存中斷現場,對於嵌套中斷,被中斷程序的現場信息依次壓入系統棧,中斷返回時逆序彈出; (2)保存操做系統子程序間相互調用的參數、返回值、返回點以及子程序(函數)的局部變量。數據結構

②用戶棧是用戶進程空間中的一塊區域,用於保存用戶進程的子程序間相互調用的參數、返回值、返回點以及子程序(函數)的局部變量。ide

咱們編寫的遞歸程序屬於用戶程序,所以使用的是用戶棧。函數

1.2 循環會快些嗎?

  遞歸與循環是兩種不一樣的解決問題的典型思路。固然也並非說循環效率就必定比遞歸高,遞歸和循環是兩碼事,遞歸帶有棧操做,循環則不必定,兩個概念不是一個層次,不一樣場景作不一樣的嘗試。測試

  (1)遞歸算法:優化

  ①優勢:代碼簡潔、清晰,而且容易驗證正確性。this

  ②缺點:它的運行須要較屢次數的函數調用,若是調用層數比較深,須要增長額外的堆棧處理(還有可能出現堆棧溢出的狀況),好比參數傳遞須要壓棧等操做,會對執行效率有必定影響。可是,對於某些問題,若是不使用遞歸,那將是極端難看的代碼。spa

  (2)循環算法:

  ①優勢:速度快,結構簡單。

  ②缺點:並不能解決全部的問題。有的問題適合使用遞歸而不是循環。可是若是使用循環並不困難的話,最好使用循環

  (3)遞歸與循環的對比總結:

  ①通常遞歸調用能夠處理的算法,也經過循環去解決常須要額外的低效處理。

  ②如今的編譯器在通過優化後,對於屢次調用的函數處理會有很是好的效率優化,效率未必低於循環。

  ③遞歸和循環二者徹底能夠互換。若是用到遞歸的地方能夠很方便使用循環替換,而不影響程序的閱讀,那麼替換成遞歸每每是好的。(例如:求階乘的遞歸實現與循環實現。)

2、二叉樹的非遞歸遍歷實現

2.1 前序遍歷的非遞歸實現

PreOrder

        // Method01:前序遍歷
        public void PreOrderNoRecurise(Node<T> node)
        {
            if (node == null)
            {
                return;
            }
            // 根->左->右
            Stack<Node<T>> stack = new Stack<Node<T>>();
            stack.Push(node);
            Node<T> tempNode = null;

            while (stack.Count > 0)
            {
                // 1.遍歷根節點
                tempNode = stack.Pop();
                Console.Write(tempNode.data);
                // 2.右子樹壓棧
                if (tempNode.rchild != null)
                {
                    stack.Push(tempNode.rchild);
                }
                // 3.左子樹壓棧(目的:保證下一個出棧的是左子樹的節點)
                if (tempNode.lchild != null)
                {
                    stack.Push(tempNode.lchild);
                }
            }
        }
View Code

  在該方法中,利用了棧的先進後出的特性,首先遍歷顯示根節點,而後將右子樹(注意是右子樹不是左子樹)壓棧,最後將左子樹壓棧。因爲最後時將左子樹節點壓棧,因此下一次首先出棧的應該是左子樹的根節點,也就保證了先序遍歷的規則。

2.2 中序遍歷的非遞歸實現

MidOrder

        public void MidOrderNoRecurise(Node<T> node)
        {
            if (node == null)
            {
                return;
            }
            // 左->根->右
            Stack<Node<T>> stack = new Stack<Node<T>>();
            Node<T> tempNode = node;

            while (tempNode != null || stack.Count > 0)
            {
                // 1.依次將全部左子樹節點壓棧
                while(tempNode != null)
                {
                    stack.Push(tempNode);
                    tempNode = tempNode.lchild;
                }
                // 2.出棧遍歷節點
                tempNode = stack.Pop();
                Console.Write(tempNode.data);
                // 3.左子樹遍歷結束則跳轉到右子樹
                tempNode = tempNode.rchild;
            }
        }
View Code

  在該方法中,首先將根節點全部的左子樹節點壓棧,而後一一出棧,每當出棧一個元素後,便將其右子樹節點壓棧。這樣就能夠實現首先出棧的永遠是棧中的左子樹節點,而後是根節點,最後時右子樹節點,也就能夠保證中序遍歷的規則。

2.3 後序遍歷的非遞歸實現

PostOrder

        public void PostOrderNoRecurise(Node<T> node)
        {
            if (root == null)
            {
                return;
            }

            // 兩個棧:一個存儲,一個輸出
            Stack<Node<T>> stackIn = new Stack<Node<T>>();
            Stack<Node<T>> stackOut = new Stack<Node<T>>();
            Node<T> currentNode = null;
            // 根節點首先壓棧
            stackIn.Push(node);
            // 左->右->根
            while (stackIn.Count > 0)
            {
                currentNode = stackIn.Pop();
                stackOut.Push(currentNode);
                // 左子樹壓棧
                if (currentNode.lchild != null)
                {
                    stackIn.Push(currentNode.lchild);
                }
                // 右子樹壓棧
                if (currentNode.rchild != null)
                {
                    stackIn.Push(currentNode.rchild);
                }
            }

            while (stackOut.Count > 0)
            {
                // 依次遍歷各節點
                Node<T> outNode = stackOut.Pop();
                Console.Write(outNode.data);
            }
        }
View Code

  在該方法中,使用了兩個棧來輔助,其中一個stackIn做爲中間存儲起到過渡做用,而另外一個stackOut則做爲最後的輸出結果進行遍歷顯示。衆所周知,棧的特性使LIFO(後進先出),那麼stackIn在進行存儲過渡時,先按照根節點->左孩子->右孩子的順序依次壓棧,那麼其出棧順序就是右孩子->左孩子->根節點。而每當循環一次就會從stackIn中出棧一個元素,並壓入stackOut中,那麼這時stackOut中的出棧順序則變成了左孩子->右孩子->根節點的順序,也就符合了後序遍歷的規則。

2.4 層次遍歷的實現

LevelOrder

        public void LevelOrder(Node<T> node)
        {
            if (root == null)
            {
                return;
            }

            Queue<Node<T>> queueNodes = new Queue<Node<T>>();
            queueNodes.Enqueue(node);
            Node<T> tempNode = null;
            // 利用隊列先進先出的特性存儲節點並輸出
            while (queueNodes.Count > 0)
            {
                tempNode = queueNodes.Dequeue();
                Console.Write(tempNode.data);

                if (tempNode.lchild != null)
                {
                    queueNodes.Enqueue(tempNode.lchild);
                }

                if (tempNode.rchild != null)
                {
                    queueNodes.Enqueue(tempNode.rchild);
                }
            }
        }
View Code

  在該方法中,使用了一個隊列來輔助實現,隊列是遵循FIFO(先進先出)的,與棧恰好相反,因此,咱們這裏只須要按照根節點->左孩子->右孩子的入隊順序依次入隊,輸出時就能夠符合根節點->左孩子->右孩子的規則了。

2.5 各類非遞歸遍歷的測試

   上面咱們實現了非遞歸方式的遍歷算法,這裏咱們對其進行一個簡單的測試。跟上一篇相同首先建立一棵以下圖所示的二叉樹,而後調用非遞歸版的先序、中序、後序以及層次遍歷方法查看遍歷結果。

binary tree

  (1)測試代碼:

        static void MyBinaryTreeBasicTest()
        {
            // 構造一顆二叉樹,根節點爲"A"
            MyBinaryTree<string> bTree = new MyBinaryTree<string>("A");
            Node<string> rootNode = bTree.Root;
            // 向根節點"A"插入左孩子節點"B"和右孩子節點"C"
            bTree.InsertLeft(rootNode, "B");
            bTree.InsertRight(rootNode, "C");
            // 向節點"B"插入左孩子節點"D"和右孩子節點"E"
            Node<string> nodeB = rootNode.lchild;
            bTree.InsertLeft(nodeB, "D");
            bTree.InsertRight(nodeB, "E");
            // 向節點"C"插入右孩子節點"F"
            Node<string> nodeC = rootNode.rchild;
            bTree.InsertRight(nodeC, "F");
            // 計算二叉樹目前的深度
            Console.WriteLine("The depth of the tree : {0}", bTree.GetDepth(bTree.Root));

            // 前序遍歷
            Console.WriteLine("---------PreOrder---------");
            bTree.PreOrder(bTree.Root);
            // 中序遍歷
            Console.WriteLine();
            Console.WriteLine("---------MidOrder---------");
            bTree.MidOrder(bTree.Root);
            // 後序遍歷
            Console.WriteLine();
            Console.WriteLine("---------PostOrder---------");
            bTree.PostOrder(bTree.Root);
            Console.WriteLine();
            // 前序遍歷(非遞歸)
            Console.WriteLine("---------PreOrderNoRecurise---------");
            bTree.PreOrderNoRecurise(bTree.Root);
            // 中序遍歷(非遞歸)
            Console.WriteLine();
            Console.WriteLine("---------MidOrderNoRecurise---------");
            bTree.MidOrderNoRecurise(bTree.Root);
            // 後序遍歷(非遞歸)
            Console.WriteLine();
            Console.WriteLine("---------PostOrderNoRecurise---------");
            bTree.PostOrderNoRecurise(bTree.Root);
            Console.WriteLine();
            // 層次遍歷
            Console.WriteLine("---------LevelOrderNoRecurise---------");
            bTree.LevelOrder(bTree.Root);
        }
View Code

  (2)運行結果:

3、二叉查找樹又是什麼鬼?

  二叉查找樹(Binary Search Tree)又稱二叉排序樹(Binary Sort Tree),亦稱二叉搜索樹。它具備如下幾個性質:

  (1)若左子樹不空,則左子樹上全部結點的值均小於它的根結點的值;

  (2)若右子樹不空,則右子樹上全部結點的值均大於或等於它的根結點的值;

  (3)左、右子樹也分別爲二叉排序樹;

  (4)沒有鍵值相等的節點。

  對於二叉查找樹,咱們只須要進行一次中序遍歷即可以獲得一個排序後的遍歷結果。

4、二叉查找樹的實現

4.1 新節點的插入

  二叉查找樹的插入過程大體爲如下幾個步驟:

  Step1.若當前的二叉查找樹爲空,則插入的元素爲根節點;

  --> Step2.若插入的元素值小於根節點值,則將元素插入到左子樹中;

    --> Step3.若插入的元素值不小於根節點值,則將元素插入到右子樹中。

        public void InsertNode(int data)
        {
            Node newNode = new Node();
            newNode.data = data;

            if (this.root == null)
            {
                this.root = newNode;
            }
            else
            {
                Node currentNode = this.root;
                Node parentNode = null;

                while(currentNode != null)
                {
                    parentNode = currentNode;
                    if(currentNode.data < data)
                    {
                        currentNode = currentNode.rchild;
                    }
                    else
                    {
                        currentNode = currentNode.lchild;
                    }
                }

                if(parentNode.data < data)
                {
                    // 若插入的元素值小於根節點值,則將元素插入到左子樹中
                    parentNode.rchild = newNode;
                }
                else
                {
                    // 若插入的元素值不小於根節點值,則將元素插入到右子樹中
                    parentNode.lchild = newNode;
                }
            }
        }
View Code

  對如上圖所示的二叉查找樹進行構造:

     MyBinarySearchTree bst = new MyBinarySearchTree(8);
     bst.InsertNode(3);
     bst.InsertNode(10);
     bst.InsertNode(1);
     bst.InsertNode(6);
     bst.InsertNode(14);
     bst.InsertNode(4);
     bst.InsertNode(7);
     bst.InsertNode(13);

     Console.WriteLine("----------LevelOrder----------");
     bst.LevelOrder(bst.Root);

  層次遍歷的顯示結果以下圖所示:

4.2 老節點的移除

  二叉查找樹的刪除過程相比插入過程要複雜一些,這裏主要分三種狀況進行處理:

  Scene1.節點p爲葉子節點:直接刪除該節點,再修改其父節點的指針(注意分是根節點和不是根節點),如圖(a);

  Scene2.節點p爲單支節點(即只有左子樹或右子樹):讓p的子樹與p的父親節點相連,再刪除p便可;(注意分是根節點和不是根節點兩種狀況),如圖b;

  Scene3.節點p的左子樹和右子樹均不爲空:首先找到p的後繼y,由於y必定沒有左子樹,因此能夠刪除y,並讓y的父親節點成爲y的右子樹的父親節點,並用y的值代替p的值;或者能夠先找到p的前驅x,x必定沒有右子樹,因此能夠刪除x,並讓x的父親節點成爲y的左子樹的父親節點。如圖c。

  經過代碼實現以下:

        public void RemoveNode(int key)
        {
            Node current = null, parent = null;

            // 定位節點位置
            current = FindNode(key);

            // 沒找到data爲key的節點
            if (current == null)
            {
                Console.WriteLine("沒有找到data爲{0}的節點!", key);
                return;
            }

            #region 1.若是該節點是葉子節點
            if (current.lchild == null && current.rchild == null) // 若是該節點是葉子節點
            {
                if (current == this.root) // 若是該節點爲根節點
                {
                    this.root = null;
                }
                else if (parent.lchild == current) // 若是該節點爲左孩子節點
                {
                    parent.lchild = null;
                }
                else if (parent.rchild == current) // 若是該節點爲右孩子節點
                {
                    parent.rchild = null;
                }
            } 
            #endregion
            #region 2.若是該節點是單支節點
            else if (current.lchild == null || current.rchild == null) // 若是該節點是單支節點 (只有一個左孩子節點或者一個右孩子節點)
            {
                if (current == this.root) // 若是該節點爲根節點
                {
                    if (current.lchild == null)
                    {
                        this.root = current.rchild;
                    }
                    else
                    {
                        this.root = current.lchild;
                    }
                }
                else
                {
                    if (parent.lchild == current && current.lchild != null)  // p是q的左孩子且p有左孩子
                    {
                        parent.lchild = current.lchild;
                    }
                    else if (parent.lchild == current && current.rchild != null) // p是q的左孩子且p有右孩子
                    {
                        parent.rchild = current.rchild;
                    }
                    else if (parent.rchild == current && current.lchild != null) // p是q的右孩子且p有左孩子
                    {
                        parent.rchild = current.lchild;
                    }
                    else // p是q的右孩子且p有右孩子
                    {
                        parent.rchild = current.rchild;
                    }
                }
            } 
            #endregion
            #region 3.若是該節點的左右子樹均不爲空 
            else // 若是該節點的左右子樹均不爲空 
            {
                Node t = current;
                Node s = current.lchild; // 從p的左子節點開始 
                // 找到p的前驅,即p左子樹中值最大的節點 
                while(s.rchild != null)
                {
                    t = s;
                    s = s.rchild;
                }

                current.data = s.data; // 把節點s的值賦給p

                if (t == current)
                {
                    current.lchild = s.lchild;
                }
                else
                {
                    current.rchild = s.rchild;
                }
            } 
            #endregion
        }

        // 根據Key查找某個節點
        public Node FindNode(int key)
        {
            Node currentNode = this.root;
            while (currentNode != null && currentNode.data != key)
            {
                if (currentNode.data < key)
                {
                    currentNode = currentNode.rchild;
                }
                else if (currentNode.data > key)
                {
                    currentNode = currentNode.lchild;
                }
                else
                {
                    break;
                }
            }

            return currentNode;
        }
View Code

  在上面的示例中移除既有左孩子又有右孩子的節點6後的層次遍歷結果以下圖所示:

附件下載

  本文所實現的C#版二叉樹的代碼:http://pan.baidu.com/s/1gdjKwKF

參考資料

(1)程傑,《大話數據結構》

(2)陳廣,《數據結構(C#語言描述)》

(3)段恩澤,《數據結構(C#語言版)》

(4)VincentCZW,《遞歸的效率問題以及與循環的比較

(5)HelloWord,《循環與遞歸的區別

(6)愛也玲瓏,《二叉查找樹—插入、刪除與查找

 

相關文章
相關標籤/搜索