資料結構基礎溫故-4.樹與二叉樹(中)
在上一篇中,我們瞭解了樹的基本概念以及二叉樹的基本特點和程式碼實現,還用遞迴的方式對二叉樹的三種遍歷演算法進行了程式碼實現。但是,由於遞迴需要系統堆疊,所以空間消耗要比非遞迴程式碼要大很多。而且,如果遞迴深度太大,可能系統撐不住。因此,我們使用非遞迴(這裡主要是迴圈,迴圈方法比遞迴方法快, 因為迴圈避免了一系列函式呼叫和返回中所涉及到的引數傳遞和返回值的額外開銷)來重新實現一遍各種遍歷演算法,再對二叉樹的另外一種特殊的遍歷—層次遍歷進行實現,最後再瞭解一下特殊的二叉樹—二叉查詢樹。
一、遞迴與迴圈的區別及比較
1.1 遞迴為何很慢?
大家都知道遞迴的實現是通過呼叫函式本身,函式呼叫的時候,每次呼叫時要做地址儲存,引數傳遞等,這是通過一個遞迴工作棧
關於系統棧和使用者棧:
①系統棧(也叫核心棧、核心棧)是記憶體中屬於作業系統空間的一塊區域,其主要用途為: (1)儲存中斷現場,對於巢狀中斷,被中斷程式的現場資訊依次壓入系統棧,中斷返回時逆序彈出; (2)儲存作業系統子程式間相互呼叫的引數、返回值、返回點以及子程式(函式)的區域性變數。
②使用者棧是使用者程序空間中的一塊區域,用於儲存使用者程序的子程式間相互呼叫的引數、返回值、返回點以及子程式(函式)的區域性變數。
我們編寫的遞迴程式屬於使用者程式,因此使用的是使用者棧。
1.2 迴圈會快些嗎?
遞迴與迴圈是兩種不同的解決問題的典型思路。當然也並不是說迴圈效率就一定比遞迴高,遞迴和迴圈是兩碼事,遞迴帶有棧操作,迴圈則不一定,兩個概念不是一個層次,不同場景做不同的嘗試。
(1)遞迴演算法:
①優點:程式碼簡潔、清晰,並且容易驗證正確性。
②缺點:它的執行需要較多次數的函式呼叫,如果呼叫層數比較深,需要增加額外的堆疊處理(還有可能出現堆疊溢位的情況),比如引數傳遞需要壓棧等操作,會對執行效率有一定影響。但是,對於某些問題,如果不使用遞迴,那將是極端難看的程式碼。
(2)迴圈演算法:
①優點:速度快,結構簡單。
②缺點:並不能解決所有的問題。有的問題適合使用遞迴而不是迴圈。但是如果使用迴圈並不困難的話,最好使用迴圈。
(3)遞迴與迴圈的對比總結:
①一般遞迴呼叫可以處理的演算法,也通過迴圈去解決常需要額外的低效處理。
②現在的編譯器在經過優化後,對於多次呼叫的函式處理會有非常好的效率優化,效率未必低於迴圈。
③遞迴和迴圈兩者完全可以互換。如果用到遞迴的地方可以很方便使用迴圈替換,而不影響程式的閱讀,那麼替換成遞迴往往是好的。(例如:求階乘的遞迴實現與迴圈實現。)
二、二叉樹的非遞迴遍歷實現
2.1 前序遍歷的非遞迴實現
// 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 中序遍歷的非遞迴實現
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 後序遍歷的非遞迴實現
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 層次遍歷的實現
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 各種非遞迴遍歷的測試
上面我們實現了非遞迴方式的遍歷演算法,這裡我們對其進行一個簡單的測試。跟上一篇相同首先建立一棵如下圖所示的二叉樹,然後呼叫非遞迴版的先序、中序、後序以及層次遍歷方法檢視遍歷結果。
(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)執行結果:
三、二叉查詢樹又是什麼鬼?
二叉查詢樹(Binary Search Tree)又稱二叉排序樹(Binary Sort Tree),亦稱二叉搜尋樹。它具有以下幾個性質:
(1)若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值;
(2)若右子樹不空,則右子樹上所有結點的值均大於或等於它的根結點的值;
(3)左、右子樹也分別為二叉排序樹;
(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後的層次遍歷結果如下圖所示:
附件下載
參考資料
(1)程傑,《大話資料結構》
(2)陳廣,《資料結構(C#語言描述)》
(3)段恩澤,《資料結構(C#語言版)》
(5)HelloWord,《迴圈與遞迴的區別》
作者:周旭龍
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。
相關推薦
資料結構基礎溫故-4.樹與二叉樹(下)
上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表示式的值。首先,將表示式轉換為二叉樹,然後通過
資料結構基礎溫故-4.樹與二叉樹(中)
在上一篇中,我們瞭解了樹的基本概念以及二叉樹的基本特點和程式碼實現,還用遞迴的方式對二叉樹的三種遍歷演算法進行了程式碼實現。但是,由於遞迴需要系統堆疊,所以空間消耗要比非遞迴程式碼要大很多。而且,如果遞迴深度太大,可能系統撐不住。因此,我們使用非遞迴(這裡主要是迴圈,迴圈方法比遞迴方法快, 因為迴圈避免了一系
資料結構基礎溫故-4.樹與二叉樹(上)
前面所討論的線性表元素之間都是一對一的關係,今天我們所看到的結構各元素之間卻是一對多的關係。樹在計算機中有著廣泛的應用,甚至在計算機的日常使用中,也可以看到樹形結構的身影,如下圖所示的Windows資源管理器和應用程式的選單都屬於樹形結構。樹形結構是一種典型的非線性結構,除了用於表示相鄰關係外,還可以表示層次
資料結構基礎溫故-6.查詢(上):基本查詢與樹表查詢
只要你開啟電腦,就會涉及到查詢技術。如炒股軟體中查股票資訊、硬碟檔案中找照片、在光碟中搜DVD,甚至玩遊戲時在記憶體中查詢攻擊力、魅力值等資料修改用來作弊等,都要涉及到查詢。當然,在網際網路上查詢資訊就更加是家常便飯。查詢是計算機應用中最常用的操作之一,也是許多程式中最耗時的一部分,查詢方法的優劣對於系統的執
資料結構基礎溫故-5.圖(中):圖的遍歷演算法
上一篇我們瞭解了圖的基本概念、術語以及儲存結構,還對鄰接表結構進行了模擬實現。本篇我們來了解一下圖的遍歷,和樹的遍歷類似,從圖的某一頂點出發訪問圖中其餘頂點,並且使每一個頂點僅被訪問一次,這一過程就叫做圖的遍歷(Traversing Graph)。如果只訪問圖的頂點而不關注邊的資訊,那麼圖的遍歷十分簡單,使用
資料結構基礎溫故-1.線性表(中)
在上一篇中,我們學習了線性表最基礎的表現形式-順序表,但是其存在一定缺點:必須佔用一整塊事先分配好的儲存空間,在插入和刪除操作上需要移動大量元素(即操作不方便),於是不受固定儲存空間限制並且可以進行比較快捷地插入和刪除操作的連結串列橫空出世,所以我們就來複習一下連結串列。 一、單鏈表基礎 1.1 單鏈表的
資料結構基礎溫故-6.查詢(下):雜湊表
雜湊(雜湊)技術既是一種儲存方法,也是一種查詢方法。然而它與線性表、樹、圖等結構不同的是,前面幾種結構,資料元素之間都存在某種邏輯關係,可以用連線圖示表示出來,而雜湊技術的記錄之間不存在什麼邏輯關係,它只與關鍵字有關聯。因此,雜湊主要是面向查詢的儲存結構。雜湊技術最適合的求解問題是查詢與給定值相等的記錄。
資料結構基礎溫故-5.圖(中):最小生成樹演算法
圖的“多對多”特性使得圖在結構設計和演算法實現上較為困難,這時就需要根據具體應用將圖轉換為不同的樹來簡化問題的求解。 一、生成樹與最小生成樹 1.1 生成樹 對於一個無向圖,含有連通圖全部頂點的一個極小連通子圖成為生成樹(Spanning Tree)。其本質就是從連通圖任一頂點出發進行遍歷操作所經過
資料結構基礎溫故-5.圖(上):圖的基本概念
前面幾篇已經介紹了線性表和樹兩類資料結構,線性表中的元素是“一對一”的關係,樹中的元素是“一對多”的關係,本章所述的圖結構中的元素則是“多對多”的關係。圖(Graph)是一種複雜的非線性結構,在圖結構中,每個元素都可以有零個或多個前驅,也可以有零個或多個後繼,也就是說,元素之間的關係是任意的。現實生活中的很多
資料結構基礎溫故-5.圖(下):最短路徑
圖的最重要的應用之一就是在交通運輸和通訊網路中尋找最短路徑。例如在交通網路中經常會遇到這樣的問題:兩地之間是否有公路可通;在有多條公路可通的情況下,哪一條路徑是最短的等等。這就是帶權圖中求最短路徑的問題,此時路徑的長度不再是路徑上邊的數目總和,而是路徑上的邊所帶權值的和。帶權圖分為無向帶權圖和有向帶權圖,但如
資料結構基礎溫故-2.棧
現實生活中的事情往往都能總結歸納成一定的資料結構,例如餐館中餐盤的堆疊和使用,羽毛球筒裡裝的羽毛球等都是典型的棧結構。而在.NET中,值型別線上程棧上進行分配,引用型別在託管堆上進行分配,本文所說的“棧”正是這種資料結構。棧和佇列都是常用的資料結構,它們的邏輯結構與線性表相通,不同之處則在於操作受某種特殊限制
資料結構基礎溫故-1.線性表(下)
在上一篇中,我們瞭解了單鏈表與雙鏈表,本次將單鏈表中終端結點的指標端由空指標改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)。 一、迴圈連結串列基礎 1.1 迴圈連結串列節點結構 迴圈連結串列和單鏈表的
資料結構基礎溫故-3.佇列
在日常生活中,佇列的例子比比皆是,例如在車展排隊買票,排在隊頭的處理完離開,後來的必須在隊尾排隊等候。在程式設計中,佇列也有著廣泛的應用,例如計算機的任務排程系統、為了削減高峰時期訂單請求的訊息佇列等等。與棧類似,佇列也是屬於操作受限的線性表,不過佇列是隻允許在一端進行插入,在另一端進行刪除。在其他資料結構如
資料結構基礎溫故-7.排序
排序(Sorting)是計算機內經常進行的一種操作,其目的是將一組“無序”的記錄序列調整為按關鍵字“有序”的記錄序列。如何進行排序,特別是高效率地進行排序時計算機工作者學習和研究的重要課題之一。排序有內部排序和外部排序之分,若整個排序過程不需要訪問外存便能完成,則稱此類排序為內部排序,反之則為外部排序。本篇主
3、非線性結構--樹與二叉樹——數據結構【基礎篇】
位置 enter 深度 基礎 表達式 左右 -a 基礎篇 先序遍歷 非線性結構--樹與二叉樹 二叉樹的基礎知識: 二叉樹的特點: 1、每個結點的度<=2 2、二叉樹是有序樹 二叉樹的五種不
Android版資料結構與演算法(六):樹與二叉樹
/** * 前序遍歷——迭代 * @author Administrator * */ public void preOrder(TreeNode node){ if(node == null){ return;
資料結構之樹與二叉樹(下)
上面兩篇我們瞭解了樹的基本概念以及二叉樹的遍歷演算法,還對二叉查詢樹進行了模擬實現。數學表示式求值是程式設計語言編譯中的一個基本問題,表示式求值是棧應用的一個典型案例,表示式分為字首、中綴和字尾三種形式。這裡,我們通過一個四則運算的應用場景,藉助二叉樹來幫助求解表
【資料結構與演算法】002—樹與二叉樹(Python)
概念 樹 樹是一類重要的非線性資料結構,是以分支關係定義的層次結構 定義: 樹(tree)是n(n>0)個結點的有限集T,其中: 有且僅有一個特定的結點,稱為樹的根(root) 當n>1時,其餘結點可分為m(m>0)個互不相交的有限集T1,T2,……Tm,其中每一個集合本身又是一棵
資料結構樹與二叉樹例題
樹與二叉樹例題 例1 高度為K(K>=2)的完全二叉樹至少有()個葉子結點。 解: 根據二叉樹性質 二叉樹第i(i>=1)層上至多有2^(i-1)個結點 第K-1層有 2^(K-1-1)=2^(K-2) 個結點 求二叉樹至少有多
資料結構之樹與二叉樹
前幾天被面試官問到了二叉樹,因為沒有去複習所以回答的很糟糕,資料結構是大二的時候學的,在平時的web開發我能用到的機會其實不多,所以也沒有去整理,但是資料結構也是程式的靈魂架構,是需要認真研究的,故在此繼續進行整理複習。 一、什麼是樹?1.1 樹是n(n>=0)