在上一篇博客中,我們主要介紹了四種查找的方法,包括順序查找、折半查找、插入查找以及Fibonacci查找。上面這幾種查找方式都是基於線性表的查找方式,今天博客中我們來介紹一下基於二叉樹結構的查找,也就是我們今天要聊的二叉排序樹。今天主要聊的是二叉排序樹的查找、插入與刪除的內容,二叉排序的創建過程其實就是不斷查找與插入的過程,也就是說當我們在創建二叉排序樹時,我們會先搜索該節點在二叉排序樹中的位置,若沒有找到該節點則返回該節點將要插入的父節點,然后將該結點插入。而二叉排序樹結點的刪除則有些復雜,分為幾種情況討論,下方會給出詳細的介紹。
在本篇博客的開頭,我們先聊聊什么是二叉排序樹。說的直白一些,具有左子樹上的值<根節點的值<右子樹上的值的二叉樹,我們稱之為二叉排序樹。基於二叉排序樹的特點,有結合着中序遍歷的特點(中序遍歷是先遍歷左子樹,再遍歷根節點,然后遍歷右子樹),我們不難發現,二叉排序樹的中序遍歷的結果是從小到大有序的。下方我們會先給出二叉排序樹的創建,然后給出二叉排序樹的節點刪除的代碼。廢話少說,進入今天博客的主題。
一、二叉排序樹的創建
上面也簡單的提了一下,二叉排序樹的創建無非就是不斷查找和插入的過程,當我們查找某個值沒有找到時,我們就會將該值插入到二叉排序樹中。因為再查找的過程中可以確定該結點要插入的合適位置,所以插入就顯得比較簡單了。下方我們會先給出二叉排序樹查找與插入的示意圖,然后再給出相應的代碼實現。最后給出中序遍歷的結果,觀察我們創建的二叉排序樹的中序遍歷是否是有序的。
1、二叉排序樹的查找與插入的示意圖
我們要將集合{62, 88, 58, 47, 35, 73, 51, 99, 37, 93}中的元素放入到我們的二叉排序樹中去存儲,如果對我們創建好的二叉排序樹進行中序搜索的話,輸出的結果就是上面集合的有序序列。下方就是我們二叉排序樹從無到有的完整創建過程。
-
(1)、在初始化狀態下我們二叉排序樹的根節點為空,我們依次將集合中的元素通過搜索插入到二叉排序樹中合適的位置。
-
(2)、首先在二叉排序中進行搜索62的位置,樹為空,所以將62存入到二叉排序樹的根節點中,及 root=(62)。
-
(3)、從集合中取出88,然后查找我們的二叉排序樹,發現88大於我們的根節點62,所以將88插入到62節點的右子樹中,即 (62)->rightChild=(88)。
-
(4)、從集合中取出58,然后從根節點開始查找我們現有的二叉排序樹,發現55<62,將55作為62的左結點,即 (62)->leftChild=(55)。
-
(5)、從集合中取出47,然后對二叉排序樹進行搜索,發現47<55, 所以 (55)->leftChild=(47)。
-
(6)、從集合中取出35,再次對二叉排序樹進行搜索,發現35又小於47,所以 (47)->leftChild=(35)。
-
(7)、從集合中取出73,再次對二叉排序樹進行搜索,發現62<73<88, 所以有 (88)->leftChild=(73)。
-
以此類推,要做的事情就是不斷從集合中取值,然后對二叉排序樹進行查找,找到合適的插入點,然后將相應的節點進行插入,具體步驟就不做過多贅述了。
2.二叉排序樹創建的代碼實現
接下來我們就來實現二叉排序樹創建的代碼實現的階段了,二叉排序樹創建分為幾個步驟,第一步創建二叉樹的結點,第二步二叉排序樹的搜索,第三步則是結點的插入。接下來我們將要慢慢的來實現這幾個過程。
(1)、二叉排序樹的結點類
下方這段代碼就是二叉排序樹的結點類,該類的結構與之前我們聊二叉樹時的結構沒什么區別。因為二叉排序樹的物理存儲結構也是通過二叉鏈表的形式來組織的,所以下方的BinaryTreeNote中data字段用於存儲結點數據,leftChild用來指向左孩子,rightChild用來指向右孩子。二叉樹更詳細的特性請參考之前發布的博客吧《二叉樹的遍歷及其線索化》,在此就不做過多贅述了。
(2)、查找二叉排序樹代碼實現
首先我們先創建一個類,也就是下方的SearchResult類,該類中存儲的就是每次查找返回的結果。下方就是對每個結點的介紹:
-
searchNote字段存儲的就是查找到的節點,如果未查找到,那么該結點就為空。
-
fatherNote字段就負責存儲當前查到節點的父節點,如果該節點為空,那么當前查到的節點就為根節點。如果查找失敗,那么我們的結點將要掛到fatherNote節點上。
-
isFound字段負責存儲查找的結果,如果二叉排序樹中有要查找的節點,那么該字段為true, 如果沒有,該字段就為false。
實現完查找結果的存儲類后,接下來我們就該實現我們的查找方法了。下方這個searchBST()方法就是我們二叉排序樹的查找方法,該方法有三個參數,第一個參數currentRoot是當前二叉排序樹的根節點,當然在每次遞歸遍歷時該參數就是每輪遞歸時子樹的根節點。第二個參數是fatherNote,也就是第一個參數currentRoot的父節點,如果currentRoot是整個二叉排序樹的根節點的話,那么fatherNote就為空。而第三個參數就是我們要匹配的關鍵字key。該方法的返回值就是上面SearchResult的對象,該對象中存儲的就是查找的相關結果。
下方代碼主要分為下方幾步:
-
首先創建存儲查找結果的對象 searchResult,以備下方查找時使用。
-
然后判斷 currentRoot是否為空,如果為nil說明沒有找到key相應的結點。根據此結果設置 searchResult的值,並返回 searchResult。
-
緊接着在判斷key是否等於 currentRoot.data, 如果等於就說明我們找到了相應的結點,根據此結果設置 searchResult的值,並返回 searchResult對象。
-
如果沒有查找失敗,也沒有查找成功,我們就比較key是否小於 currentRoot.data,如果小於的話,說明我們的key有可能位於左子樹上,然后遞歸查找左子樹。
-
如果key大於 currentRoot.data, 那么說明我們的key有可能位於右子樹上,遞歸查找右子樹。
(3)、二叉排序樹節點的插入操作
二叉排序樹的插入操作就顯得比較簡單了,因為再查找的返回結果中,如果查找失敗,那么返回結果中也會有fatherNote。這個FatherNote就是我們將要插入節點的父節點。不過二叉排序樹為空樹時,查找結果的fatherNote為空,所以我們先判斷fatherNote節點是否為空,如果為nil的話,我們就把當前關鍵字key對應的結點作為二叉排序樹的根結點。如果fatherNote不為nil, 那么我們就判斷key是否大於fatherNote.data, 如果大於的話,我們就把key作為fatherNote的右結點,否則作為fatherNote的左結點。具體代碼如下所示。
(4)、二叉排序樹的創建
上面我們實現了二叉排序樹的搜索和插入的代碼,上面我們不止一次的提到過,二叉排序樹的創建就是不斷查找和插入的過程。也就是先對要插入的結點key進行查找,如果二叉排序樹上沒有該key的話,就需要根據查找結果將key插入的二叉排序樹中相應的位置上。下方代碼就是二叉排序樹的創建,就是先查找,如果沒找到就插入,具體代碼如下所示:
3、創建二叉排序樹測試用例
下方就是我們創建二叉排序樹的測試用例,會將searchTable數組中的線性元素轉換成二叉排序樹。BinarySearchTree的參數就是一個線性表,該類中的構造器會調用上面的createBinarySearchTree()的方法來構建二叉排序樹。構建完二叉排序樹后,對二叉排序樹進行中序遍歷,下方輸出的就是中序遍歷的結果,從結果中我們不難看出中序遍歷的結果是從小到大有序的,由此可見我們的二叉排序樹已正確創建了。如果你不放心,你可以將其先序遍歷的結果也輸出,進行檢查。
二、二叉排序樹結點的刪除
二叉排序樹的結點刪除要比二叉排序樹結點的插入要復雜一些,不過也並不難,要分為幾種情況進行討論。二叉排序樹結點的插入與刪除都是在查找的基礎上來做的。下方我們就假設找到了我們要刪除的結點,根據結點含有的左右結點的個數來進行分類討論。下方會對這幾種情況進行討論。
1.刪除結點的幾種情況
(1)、刪除結點為葉子結點
刪除的結點沒有左子樹也沒有右子樹,也就是刪除的結點為葉子結點。這種情況下我們有可以細分為兩類,一種是該葉子結點就是二叉排序樹的根節點,也就是二叉排序樹中只有一個節點的情況。只需要將root指針置為空即可。再一種情況是有刪除的葉子節點有父節點,直接將父節點連接該刪除節點的指針置空即可。示意圖如下所示:
(2)、刪除的節點只有左子樹的情況
該情況也可以細分為兩類,一種是該刪除的結點沒有父節點,也就刪除的節點為根節點,我們需要將根節點的root指針指向即將刪除結點的左孩子,然后將刪除結點的leftChild置空即可。
如果該結點有父節點,那么將父節點相應的孩子指針指向刪除節點的左孩子,然后將刪除節點的leftChild置空。示意圖如下所示:
(3)、刪除的節點只有右子樹的情況
該情況也可以細分為兩類,一種是該刪除的結點沒有父節點,也就刪除的節點為根節點,我們需要將根節點的root指針指向即將刪除結點的右孩子,然后將刪除結點的rightChild置空即可。
如果該結點有父節點,那么將父節點相應的孩子指針指向刪除節點的右孩子,然后將刪除節點的rightChild置空。 示意圖如下所示:
(4)、刪除的節點既有左子樹也有右子樹的情況
這種情況會稍微復雜一些,我們采用覆蓋,再刪除的方式進行解決。也就是曲線解決。直接將有左子樹也有右子樹的結點干掉似乎不是很好實現,因為這樣會破壞二叉排序樹的結果。我們可以間接的去做。可以分為下方的兩步。
-
第一步:查找刪除結點右子樹中最小的那個值,也就是右子樹中位於最左方的那個結點。然后將這個結點的值的父節點記錄下來。並且將該節點的值賦給我們要刪除的結點。也就是覆蓋。
-
第二步:然后將右子樹中最小的那個結點進行刪除,該節點肯定符合上述三種情況的某一種情況,所以可以使用上述的方法進行刪除。
這樣一來我們就間接的刪除了既有左子樹也有右子樹的結點。具體示意圖如下所示
2.刪除結點的代碼實現
接下來我們要根據上述的示例圖來實現我們的代碼。上述將刪除的結點分為了四類,其實仔細分析一下,上面的前三種刪除結點的情況類似。於是乎我們在代碼實現時將前三種刪除結點的方法歸為一類處理,也就是封裝成一個函數來刪除有一個或者沒有子結點類型的結點。下方的deleteNoteHaveZeroOrOneChild()函數就是相應的方法。該函數有兩個參數,第一個就是我們查找到要刪除結點的查找結果對象,第二個參數就是該節點的子節點,如果該節點沒有子節點的話,那么該參數就為nil。
在下方函數代碼中,大體可以分為以下幾步:
-
首先調用 setNilForNote()方法將該將要刪除的結點的子節點指針置空
-
然后判斷此將要被刪除的結點是否是二叉排序樹的根節點,如果是的話,就使 rootNote指針指向該結點的子結點。
-
如果要被刪除的結點不為根節點的話,我們需要判斷要刪除結點的值是比其父節點的值是大還是小,如果是小的話,說明要刪除的結點是其父節點的左孩子,然后就要把父節點的 leftChild指針指向要刪除結點的子節點。同理如果刪除結點的值要比父節點的值要大,那么就需要將父節點的 rightChild指針指向刪除結點的子結點。
接下來我們就要實現要刪除的結點有兩個子節點的情況,這種情況上面我們已經分析過了,實現起來並不復雜。下方這個deleteNoteHaveTwoChild()方法就是刪除有兩個子節點的情況,該方法的參數是查找的我們要刪除的結點的查找結果。其實就是查找,替換和刪除三個步驟。下方代碼可以分為以下幾步:
-
首先初始化將被刪除的結點的右子樹的最左邊結點的查找結果對象。
-
然后便利要刪除結點的右子樹,找到右子樹上最左邊的結點,也是右子書上最小的那個結點,並將相應信息存儲到我們的查找結果對象中。
-
將右子樹中最小的值賦值給我們要刪除的結點,然后調用上面的方法將該右子樹上的最小結點刪除即可。
三、測試用例
經過上的所有步驟,我們的二叉排序樹的查找、插入、刪除實現完畢。接下來又到了測試的時間了,下方就是我們本篇博客的測試用例。首先我們通過線性表來創建二叉排序,如何依次刪除99,35,37,62這些節點,這些節點有葉子節點,有的只有左子樹,有的也只有右子樹,有的既有左子樹也有右子樹。
上述代碼的輸出結果為,從最后一個輸出結果我們可以看出,我們要刪除的結點62既有左子樹也有右子樹,所以尋找62右子樹上最小的值73,然后將62進行覆蓋。最后把62右子樹上的73進行釋放掉即可。
本篇博客只對二叉排序樹的核心代碼進行了介紹,完整示例請移步github, 本篇博客中所有代碼都會在github上進行分享,分享地址如下所示:
github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/BinarySearchTree