挑戰408——數據結構(21)——二叉搜索樹的插入和刪除

在二叉搜索樹種最重要的就是插入和刪除操作了

二叉搜索樹的插入

我們要在二叉搜索樹中執行各種操作的前提就是,我們首先要有一棵二叉搜索樹。那麼,如何創建一棵二叉搜索樹呢?最簡單的方法就是我們可以從一棵空樹開始,每次調用一個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(下圖中的&t)就是dwarfTree(樹根)的一個引用,或者說是它的地址:
在這裏插入圖片描述
第一句if代碼檢測&node是否爲空樹,此時爲true,那麼我們就執行:

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

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

add(s,node -> right);

在這裏插入圖片描述
當通過add方法添加空節點的時候,就將Grumpy的右指針指向這個空節點。在函數返回的時候再將具體的內容填入該節點中:
在這裏插入圖片描述
在這裏插入圖片描述
此外,對addNode的調用將創建新的節點,並將其插入結構中,從而保留了二進制搜索樹所需的排序約束。 例如,如果按Doc,Bashful,Dopey,Happy和Sneezy的順序插入其餘五個單詞,則最終可以得到以下的二叉樹:
在這裏插入圖片描述
對應於下圖:
在這裏插入圖片描述

利用指向指針的指針
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));
   }
}

上面的代碼看起來會比較的抽象,我們還是圖解一下,假設我們要執行add(5):
在這裏插入圖片描述
首先我們的node爲變量存着樹根的地址,*node爲指向樹根的指針的指針。如圖所示。當根爲空的時候,賦值。

當爲非空的時候,比較值,然後遞歸調用add方法。當根爲空的時候新建一個節點,並賦值。然後將*node指向這個新增的節點:
在這裏插入圖片描述
在這裏插入圖片描述

注意,在此過程中node*的指向一直都在變化的!

二叉搜索樹的移除操作

二叉搜索樹的移除是很麻煩的,因爲他要考慮三種情況,假如我們有下面一棵樹:
在這裏插入圖片描述1. 當要刪除的節點是葉子的時候,我們直接刪除,因爲並不影響二叉搜索樹的結構
在這裏插入圖片描述
2. 當要刪除的節點含有一個孩子的時候,先將節點刪除,再將指向該節點的指針,指向該節點的孩子。(如刪除sleep操作)
在這裏插入圖片描述
3. 當要刪除的節點含有兩個孩子的時候,我們先將節點刪除,然後選擇左子樹中的最右節點或右子樹中的最左節點。(任意選一個,例如選擇左節點該節點保證比左側子樹中的其他任何元素都大,但小於右側子樹中的值。),然後重複此操作(如刪除此時的樹根):
在這裏插入圖片描述
在這裏插入圖片描述
於是可以有以下代碼:

//重載remove函數
Node * stringSet::remove(string s, Node *node, Node *parent){
    //遍歷相應的子樹,直到找到我們要刪除的節點
    if(s < node -> str){ //此時要刪除的節點應該在左子樹
        if(node -> left != NULL){ //如果存在左孩子
            return remove(s, node -> left, node); //遞歸尋找對應的s,並刪除
        }else{
            return NULL; //當我們要刪除的數值在樹中不存在
        }
    }else if(s > node -> str){ //此時此時要刪除的節點應該在右子樹,解釋同上
        if(node -> right != NULL){
            return remove(s, node ->right, node);
        }else{
            return NULL;
        }
    }else{ //我們找到了要刪除的節點node
        if(node ->left != NULL && node ->right != NULL){ //情況一,有兩個孩子
            node -> str = findMin(node -> right);//將此節點的值用右子樹的最小值代替
            return remove(node ->str, node ->right,node);//遞歸刪除右子樹的最小值
        }else if(parent -> left == node){
            //將父節點的左側替換爲該節點的右側或左側子節點
            //取決於哪一個存在(如果它沒有子元素,則右邊是NULL)
            parent -> left = (node -> left != NULL) ? node ->left : node ->right;
            return node;
        }else if(parent ->right == node){
            parent -> right = (node->left != NULL) ? node->left : node->right;
            return node;
        }
    }
    return NULL; //這一行永遠不會到達,但是沒有這一行編譯時會有問題
}

完整二叉搜索樹的代碼可以點擊 二叉搜索樹的操作實現C++代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章