算法與數據結構(十) 二叉排序樹的查找、插入與刪除(Swift版)

在上一篇博客中,咱們主要介紹了四種查找的方法,包括順序查找、折半查找、插入查找以及Fibonacci查找。上面這幾種查找方式都是基於線性表的查找方式,今天博客中咱們來介紹一下基於二叉樹結構的查找,也就是咱們今天要聊的二叉排序樹。今天主要聊的是二叉排序樹的查找、插入與刪除的內容,二叉排序的建立過程其實就是不斷查找與插入的過程,也就是說當咱們在建立二叉排序樹時,咱們會先搜索該節點在二叉排序樹中的位置,若沒有找到該節點則返回該節點將要插入的父節點,而後將該結點插入。而二叉排序樹結點的刪除則有些複雜,分爲幾種狀況討論,下方會給出詳細的介紹。html

在本篇博客的開頭,咱們先聊聊什麼是二叉排序樹。說的直白一些,具備左子樹上的值<根節點的值<右子樹上的值的二叉樹,咱們稱之爲二叉排序樹。基於二叉排序樹的特色,有結合着中序遍歷的特色(中序遍歷是先遍歷左子樹,再遍歷根節點,而後遍歷右子樹),咱們不難發現,二叉排序樹的中序遍歷的結果是從小到大有序的。下方咱們會先給出二叉排序樹的建立,而後給出二叉排序樹的節點刪除的代碼。廢話少說,進入今天博客的主題。git

 

1、二叉排序樹的建立github

上面也簡單的提了一下,二叉排序樹的建立無非就是不斷查找和插入的過程,當咱們查找某個值沒有找到時,咱們就會將該值插入到二叉排序樹中。由於再查找的過程當中能夠肯定該結點要插入的合適位置,因此插入就顯得比較簡單了。下方咱們會先給出二叉排序樹查找與插入的示意圖,而後再給出相應的代碼實現。最後給出中序遍歷的結果,觀察咱們建立的二叉排序樹的中序遍歷是不是有序的。數組

 

一、二叉排序樹的查找與插入的示意圖函數

咱們要將集合{62, 88, 58, 47, 35, 73, 51, 99, 37, 93}中的元素放入到咱們的二叉排序樹中去存儲,若是對咱們建立好的二叉排序樹進行中序搜索的話,輸出的結果就是上面集合的有序序列。下方就是咱們二叉排序樹從無到有的完整建立過程。post

  • (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.二叉排序樹建立的代碼實現測試

接下來咱們就來實現二叉排序樹建立的代碼實現的階段了,二叉排序樹建立分爲幾個步驟,第一步建立二叉樹的結點,第二步二叉排序樹的搜索,第三步則是結點的插入。接下來咱們將要慢慢的來實現這幾個過程。url

 

(1)、二叉排序樹的結點類spa

下方這段代碼就是二叉排序樹的結點類,該類的結構與以前咱們聊二叉樹時的結構沒什麼區別。由於二叉排序樹的物理存儲結構也是經過二叉鏈表的形式來組織的,因此下方的BinaryTreeNotedata字段用於存儲結點數據leftChild用來指向左孩子rightChild用來指向右孩子。二叉樹更詳細的特性請參考以前發佈的博客吧《二叉樹的遍歷及其線索化》,在此就不作過多贅述了。3d

  

 

(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插入的二叉排序樹中相應的位置上。下方代碼就是二叉排序樹的建立,就是先查找,若是沒找到就插入,具體代碼以下所示:

  

 

三、建立二叉排序樹測試用例

下方就是咱們建立二叉排序樹的測試用例,會將searchTable數組中的線性元素轉換成二叉排序樹。BinarySearchTree的參數就是一個線性表,該類中的構造器會調用上面的createBinarySearchTree()的方法來構建二叉排序樹。構建完二叉排序樹後,對二叉排序樹進行中序遍歷,下方輸出的就是中序遍歷的結果,從結果中咱們不難看出中序遍歷的結果是從小到大有序的,因而可知咱們的二叉排序樹已正確建立了。若是你不放心,你能夠將其先序遍歷的結果也輸出,進行檢查。

  

 

 

 

 2、二叉排序樹結點的刪除

二叉排序樹的結點刪除要比二叉排序樹結點的插入要複雜一些,不過也並不難,要分爲幾種狀況進行討論。二叉排序樹結點的插入與刪除都是在查找的基礎上來作的。下方咱們就假設找到了咱們要刪除的結點,根據結點含有的左右結點的個數來進行分類討論。下方會對這幾種狀況進行討論。

 

1.刪除結點的幾種狀況

(1)、刪除結點爲葉子結點

刪除的結點沒有左子樹也沒有右子樹,也就是刪除的結點爲葉子結點。這種狀況下咱們有能夠細分爲兩類,一種是該葉子結點就是二叉排序樹的根節點,也就是二叉排序樹中只有一個節點的狀況。只須要將root指針置爲空便可。再一種狀況是有刪除的葉子節點有父節點,直接將父節點鏈接該刪除節點的指針置空便可。示意圖以下所示:

(2)、刪除的節點只有左子樹的狀況

該狀況也能夠細分爲兩類,一種是該刪除的結點沒有父節點,也就刪除的節點爲根節點,咱們須要將根節點的root指針指向即將刪除結點的左孩子,而後將刪除結點的leftChild置空便可。

若是該結點有父節點,那麼將父節點相應的孩子指針指向刪除節點的左孩子,而後將刪除節點的leftChild置空。示意圖以下所示: 

(3)、刪除的節點只有右子樹的狀況

該狀況也能夠細分爲兩類,一種是該刪除的結點沒有父節點,也就刪除的節點爲根節點,咱們須要將根節點的root指針指向即將刪除結點的右孩子,而後將刪除結點的rightChild置空便可。

若是該結點有父節點,那麼將父節點相應的孩子指針指向刪除節點的右孩子,而後將刪除節點的rightChild置空。 示意圖以下所示:

(4)、刪除的節點既有左子樹也有右子樹的狀況

這種狀況會稍微複雜一些,咱們採用覆蓋,再刪除的方式進行解決。也就是曲線解決。直接將有左子樹也有右子樹的結點幹掉彷佛不是很好實現,由於這樣會破壞二叉排序樹的結果。咱們能夠間接的去作。能夠分爲下方的兩步。

  • 第一步:查找刪除結點右子樹中最小的那個值,也就是右子樹中位於最左方的那個結點。而後將這個結點的值的父節點記錄下來。而且將該節點的值賦給咱們要刪除的結點。也就是覆蓋。
  • 第二步:而後將右子樹中最小的那個結點進行刪除,該節點確定符合上述三種狀況的某一種狀況,因此可使用上述的方法進行刪除。

這樣一來咱們就間接的刪除了既有左子樹也有右子樹的結點。具體示意圖以下所示

 

 

2.刪除結點的代碼實現

接下來咱們要根據上述的示例圖來實現咱們的代碼。上述將刪除的結點分爲了四類,其實仔細分析一下,上面的前三種刪除結點的狀況相似。因而乎咱們在代碼實現時將前三種刪除結點的方法歸爲一類處理,也就是封裝成一個函數來刪除有一個或者沒有子結點類型的結點。下方的deleteNoteHaveZeroOrOneChild()函數就是相應的方法。該函數有兩個參數,第一個就是咱們查找到要刪除結點的查找結果對象,第二個參數就是該節點的子節點,若是該節點沒有子節點的話,那麼該參數就爲nil。

在下方函數代碼中,大致能夠分爲如下幾步:

  • 首先調用 setNilForNote()方法將該將要刪除的結點的子節點指針置空
  • 而後判斷此將要被刪除的結點是不是二叉排序樹的根節點,若是是的話,就使 rootNote指針指向該結點的子結點。
  • 若是要被刪除的結點不爲根節點的話,咱們須要判斷要刪除結點的值是比其父節點的值是大仍是小,若是是小的話,說明要刪除的結點是其父節點的左孩子,而後就要把父節點的 leftChild指針指向要刪除結點的子節點。同理若是刪除結點的值要比父節點的值要大,那麼就須要將父節點的 rightChild指針指向刪除結點的子結點。

  

 

接下來咱們就要實現要刪除的結點有兩個子節點的狀況,這種狀況上面咱們已經分析過了,實現起來並不複雜。下方這個deleteNoteHaveTwoChild()方法就是刪除有兩個子節點的狀況,該方法的參數是查找的咱們要刪除的結點的查找結果。其實就是查找,替換和刪除三個步驟。下方代碼能夠分爲如下幾步:

  • 首先初始化將被刪除的結點的右子樹的最左邊結點的查找結果對象。
  • 而後便利要刪除結點的右子樹,找到右子樹上最左邊的結點,也是右子書上最小的那個結點,並將相應信息存儲到咱們的查找結果對象中。
  • 將右子樹中最小的值賦值給咱們要刪除的結點,而後調用上面的方法將該右子樹上的最小結點刪除便可。

  

 

3、測試用例

通過上的全部步驟,咱們的二叉排序樹的查找、插入、刪除實現完畢。接下來又到了測試的時間了,下方就是咱們本篇博客的測試用例。首先咱們經過線性表來建立二叉排序,如何依次刪除99,35,37,62這些節點,這些節點有葉子節點,有的只有左子樹,有的也只有右子樹,有的既有左子樹也有右子樹。

  

上述代碼的輸出結果爲,從最後一個輸出結果咱們能夠看出,咱們要刪除的結點62既有左子樹也有右子樹,因此尋找62右子樹上最小的值73,而後將62進行覆蓋。最後把62右子樹上的73進行釋放掉便可。

  

本篇博客只對二叉排序樹的核心代碼進行了介紹,完整示例請移步github, 本篇博客中全部代碼都會在github上進行分享,分享地址以下所示:

github分享地址:https://github.com/lizelu/DataStruct-Swift/tree/master/BinarySearchTree

相關文章
相關標籤/搜索