1. 程式人生 > >資料結構——樹(8)——二叉搜尋樹的插入和刪除操作

資料結構——樹(8)——二叉搜尋樹的插入和刪除操作

二叉搜尋樹的插入操作

我們要在二叉搜尋樹中執行各種操作的前提就是,我們首先要有一棵二叉搜尋樹。那麼,如何建立一棵二叉搜尋樹呢?最簡單的方法就是我們可以從一棵空樹開始,每次呼叫一個addNode函式,將一個新的值插入二叉搜尋樹中。但是在每次插入的時候我們都要保持樹的一個排序關係,因此我們要做的就是在插入的時候,找到我們要插入的值應該在的位置。
因此和遍歷的程式碼一樣,addNode的程式碼可以從樹根開始遞迴地進行。在每個節點上,addNode必須將要插入的值與當前節點中的值進行比較。 如果要插入的值小於當前的值,則該值屬於左子樹。相反,如果要插入的值大於當前的值,則屬於右子樹。最終,該程序將遇到一個NULL子樹,該子樹表示需要新增新節點的樹中的點。此時,addNode用一個初始化為包含新的值的新節點替換NULL指標。(也就是說這樣的做法是,先找到要插入的點,然後再生成新的節點,隨後插入樹中)


看起來挺簡單吧,但是實現的程式碼卻並非如此簡單,難點在於,我們插入值後,改變了樹的結構,因此我們的引數就要用我們的引用引數。我們最終返回的是一個指向樹的指標,因此返回的型別是指標。我們的函式就是:

方法一,利用引用引數傳遞

//第一個輔助函式
void stringSet::add(string s, Node *&node){
    if(node = NULL){                   //基礎事件(base case)
        node = new Node(s);
        count++;
    }else if(node -> 
str > s){ //用else if是因為這樣的情況只會出現某一種,提高效率 add(s, node -> left); }else if (node -> str < s){ add(s,node -> right); } }

我們先來詳細分析一下這個函式。原型的話我們看的很特別:

void stringSet::add(string s, Node *&node)

特點在於第二個引數Node *&node。如果樹為空,則addNode將建立一個新節點,初始化其欄位,然後用指向新節點的指標替換現有結構中的NULL指標。 如果樹不為空,則addNode將新數值與樹根進行比較。如果數值相等,則說明這個值已經在樹中,不需要進一步的操作。如果不等,則addNode使用比較結果來確定是將數值插入左邊還是右邊的子樹,然後進行適當的遞迴呼叫。
為了更好的理解第二個引數的原理,我們試著畫出整個過程的圖解,假設我們現在新初始化了一棵新樹:

stringSet *dwarfTree = NULL;

我們使用之前提到過的堆疊模式來分析,在呼叫上述的語句後,我們可以得到如圖所示的圖解:
這裡寫圖片描述
那當我們呼叫以下語句的時候會發生什麼呢?

addNode("Grumpy", dwarfTree);

在addNode的引數中,我們建立一個數值為Grumpy的節點,那麼&node就是dwarfTree(樹根)的一個引用,或者說是它的地址。
這裡寫圖片描述
從上到下的白色區域分別對應的是,數值Grumpy,&node,dwarfTree。
第一句if程式碼檢測&node是否為空樹,此時為true,那麼我們就執行:

node = new Node(s); //Node中有三個變數,value,left,right

這一行在堆上分配一個含值為S的新節點,並將其分配給引用引數node,從而改變呼叫者中的指標值,如下所示:
這裡寫圖片描述
上圖的結構代表了只含有一個節點Grumpy時的儲存情況。此時樹不在為空,變數dwarfTree現在包含了指向節點Grumpy的地址了。假設Sleep在Grumpy的後面,那麼程式就會呼叫:

add(s,node -> right);

這個呼叫過程跟剛剛的基本相同,唯一不同就是,引用引數node現在指向的不再是空節點,而是有某個節點地址的變量了:
這裡寫圖片描述
新的節點被分配到Grumpy的右指標

這裡寫圖片描述
程式返回的時候應該是這樣的:
這裡寫圖片描述
依次插入其他的資料,最終就變成了一棵二叉搜尋樹
這裡寫圖片描述

方法二利用指向指標的指標

先看程式碼:

void StringSet::add(string s, Node **node) {
   if (*node == nullptr) {
       *node = new Node(s);
       count++;
   } else if ((*node)->str > s) {
       add(s, &((*node)->left));
   } else if ((*node)->str < s) {
       add(s, &((*node)->right));
   }
}

同樣的,我們用圖解來解讀這一段程式碼:
這裡寫圖片描述
首先我們的node為變數存著樹根的地址,** *node為指向樹根。如圖所示。當根為空的時候,賦值。
當為非空的時候,比較值,然後遞迴呼叫add方法。**
注意,此過程中node*的指向一直都在變化的!
(假設我們要插入5)指向依次為6的左邊,2的右邊,4的右邊,找到了null指標,此時生成一個含5的節點,然後再將其地址賦給空節點:
這裡寫圖片描述

二叉搜尋樹的移除操作

前面我們提到過,移除二叉搜尋樹的時候有三種情況。我們先逐個討論(假設我們有如下的一棵樹):
這裡寫圖片描述
1. 當要刪除的節點是葉子的時候,我們直接刪除,因為並不影響二叉搜尋樹的結構
這裡寫圖片描述
2. 當要刪除的節點含有一個孩子的時候,先將節點刪除,再將指向該節點的指標,指向該節點的孩子。(如刪除sleep操作)
這裡寫圖片描述
3. 當要刪除的節點含有兩個孩子的時候,我們先將節點刪除,然後在節點的左子樹尋找最左的值(最小值)或者在節點右子樹的最右邊尋找一個值代替此時的空節點,然後重複此操作(如刪除此時的樹根):
這裡寫圖片描述