二叉搜索樹的實現與常見用法


作者按:因為教程所示圖片使用的是 github 倉庫圖片,網速過慢的朋友請移步《二叉搜索樹的實現與常見用法》原文地址。更歡迎來我的小站看更多原創內容:godbmw.com,進行“姿勢”交流 ♪(^∇^*)

1. 為什么需要二叉搜索樹?

選擇數據結構的核心在於解決問題,而不是為了使用而使用。

由於二叉搜索樹的定義和特性,它可以高效解決以下問題:

  • 查找問題:二分查找
  • 高級結構:字典結構實現
  • 數據變動:節點的插入、刪除
  • 遍歷問題:前序、中序、后序和層次遍歷
  • 數值運算:ceilfloor、找到第 n 大的元素、找到指定元素在排序好的數組的位置 等等

值得一提的是,除了遍歷算法,上述各種問題的算法時間復雜度都是 : \(O(\log_2 n)\)

2. 二叉搜索樹的定義和性質

二叉搜索樹是一顆空樹,或者具有以下性質的二叉樹:

  • 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值
  • 若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值
  • 任意節點的左、右子樹也分別為二叉查找樹
  • 沒有鍵值相等的節點

需要注意的是,二叉搜索樹不一定是一顆完全二叉樹,因此,二叉搜索樹不能用數組來存儲。

3. 二叉搜索樹的實現

第 3 部分實現的測試代碼地址:https://gist.github.com/dongyuanxin/d0803a8821c6797e9ce8522a676cf44b

這是 Github 的 GIST,請自備梯子。

3.1 樹結構實現

借助struct和指針模擬樹的結構,並且將其封裝到BST這個類之中:

// BST.h
// Created by godbmw.com on 2018/9/27.
//

#ifndef BINARYSEARCH_BST_H
#define BINARYSEARCH_BST_H

#include <iostream>
#include <queue>

using namespace std;

template <typename Key, typename Value>
class BST {
private:
    struct Node {
        Key key;
        Value value;
        Node  *left;
        Node *right;

        Node(Key key, Value value) {
            this->key = key;
            this->value = value;
            this->left = NULL;
            this->right = NULL;
        }

        Node(Node* node) {
            this->key = node->key;
            this->value = node->value;
            this->left = node->left;
            this->right = node->right;
        }
    };

    Node *root;
    int count;

public:
    BST() {
        this->root = NULL;
        this->count = 0;
    }
    ~BST() {
        this->destroy(this->root);
    }
    int size() {
        return this->count;
    }
    bool isEmpty() {
        return this->root == NULL;
    }
};

#endif //BINARYSEARCH_BST_H

3.2 實現節點插入

插入采取遞歸的寫法,思路如下:

  1. 遞歸到底層情況:新建節點,並且返回
  2. 非底層情況:如果當前鍵等於插入鍵,則更新當前節點的值;小於,進入當前節點的左子樹;大於,進入當前節點的右子樹。
private:
    Node* insert(Node* node, Key key, Value value) {
        if(node == NULL) {
            count++;
            return new Node(key, value);
        }

        if(key == node->key) {
            node->value = value;
        } else if( key < node->key) {
            node->left = insert(node->left, key, value);
        } else {
            node->right = insert(node->right, key, value);
        }
        return node;
    }

public:
    void insert(Key key, Value value) {
        this->root = this->insert(this->root, key, value);
    }

3.3 實現節點的查找

查找包含 2 個函數:containsearch。前者返回布爾型,表示樹中是否有這個節點;后者返回指針類型,表示樹中節點對應的值。

search為什么返回值的指針類型呢:

  • 如果要查找的節點不存在,指針可以直接返回NULL
  • 如果返回Node*,就破壞了類的封裝性。原則上,內部數據結構不對外展示。
  • 如果查找的節點存在,返回去鍵對應的值,用戶可以修改,並不影響樹結構。
private:
    bool contain(Node* node, Key key) {
        if(node == NULL) {
            return false;
        }
        if(key == node->key) {
            return true;
        } else if(key < node->key) {
            return contain(node->left, key);
        } else {
            return contain(node->right, key);
        }
    }

    Value* search(Node* node, Key key) {
        if(node == NULL) {
            return NULL;
        }
        if(key == node->key) {
            return &(node->value);
        } else if (key < node->key) {
            return search(node->left, key);
        } else {
            return search(node->right, key);
        }
    }
public:
    bool contain(Key key) {
        return this->contain(this->root, key);
    }

//    注意返回值類型
    Value* search(Key key) {
        return this->search(this->root, key);
    }

3.4 遍歷實現

前序、中序和后序遍歷的思路很簡單,根據定義,直接遞歸調用即可。

對於層次遍歷,需要借助隊列queue這種數據結構。思路如下:

  1. 首先,將根節點放入隊列
  2. 如果隊列不空,進入循環
  3. 取出隊列頭部元素,輸出信息。並將這個元素出隊
  4. 將這個元素非空的左右節點依次放入隊列
  5. 檢測隊列是否為空,不空的進入第 3 步;空的話,跳出循環。
private:
    void pre_order(Node* node) {
        if(node != NULL) {
            cout<<node->key<<endl;
            pre_order(node->left);
            pre_order(node->right);
        }
    }

    void in_order(Node* node) {
        if(node != NULL) {
            in_order(node->left);
            cout<<node->key<<endl;
            in_order(node->right);
        }
    }

    void post_order(Node *node) {
        if(node != NULL) {
            post_order(node->left);
            post_order(node->right);
            cout<<node->key<<endl;
        }
    }

    void level_order(Node* node) {
        if(node == NULL) {
            return;
        }
        queue<Node*> q;
        q.push(node);
        while(!q.empty()) {
            Node* node = q.front();
            q.pop();
            cout<< node->key <<endl;
            if(node->left) {
                q.push(node->left);
            }
            if(node->right) {
                q.push(node->right);
            }
        }
    }

public:
    void pre_order() {
        this->pre_order(this->root);
    }

    void in_order() {
        this->in_order(this->root);
    }

    void post_order() {
        this->post_order(this->root);
    }

    void level_order() {
        this->level_order(this->root);
    }

3.5 實現節點刪除

為了方便實現,首先封裝了獲取最小鍵值和最大鍵值的兩個方法:minimummaximum

刪除節點的原理很簡單(忘了什么名字,是一個計算機科學家提出的),思路如下:

  1. 如果左節點為空,刪除本節點,返回右節點。
  2. 如果右節點為空,刪除本節點,返回左節點。
  3. 如果左右節點都為空,是 1 或者 2 的子情況。
  4. 如果左右節點都不為空,找到當前節點的右子樹的最小節點,並用這個最小節點替換本節點。

為什么第 4 步這樣可以繼續保持二叉搜索樹的性質呢?

顯然,右子樹的最小節點,能滿足小於右子樹的所有節點,並且大於左子樹的全部節點。

如下圖所示,要刪除58這個節點,就應該用59這個節點替換:

private:
//    尋找最小鍵值
    Node* minimum(Node* node) {
        if(node->left == NULL) {
            return node;
        }
        return minimum(node->left);
    }
//    尋找最大鍵值
    Node* maximum(Node* node) {
        if(node->right == NULL) {
            return node;
        }
        return maximum(node->right);
    }
    Node* remove_min(Node* node) {
        if(node->left == NULL) {
            Node* right = node->right;
            delete node;
            count--;
            return right;
        }
        node->left = remove_min(node->left);
        return node;
    }

    Node* remove_max(Node* node) {
        if(node->right == NULL) {
            Node* left = node->left;
            delete node;
            count--;
            return left;
        }
        node->right = remove_max(node->right);
        return node;
    }
//    刪除掉以node為根的二分搜索樹中鍵值為key的節點
//    返回刪除節點后新的二分搜索樹的根
    Node* remove(Node* node, Key key) {
        if(node == NULL) {
            return NULL;
        }
        if(key < node->key) {
            node->left = remove(node->left, key);
        } else if(key > node->key){
            node->right = remove(node->right, key);
        } else {
//            key == node->key
            if(node->left == NULL) {
                Node* right = node->right;
                delete node;
                count--;
                return right;
            }
            if(node->right == NULL) {
                Node *left = node->left;
                delete node;
                count--;
                return left;
            }
//            node->right != NULL && node->left != NULL
            Node* successor = new Node(minimum(node->right));
            count++;
//            "count --" in "function remove_min(node->right)"
            successor->right = remove_min(node->right);
            successor->left = node->left;
            delete node;
            count--;
            return successor;
        }
        return node;
    }
public:
//    尋找最小鍵值
    Key* minimum() {
        if(this->count == 0) return NULL;
        Node* min_node = this->minimum(this->root);
        return &(min_node->key);
    }

//    尋找最大鍵值
    Key* maximum() {
        if(this->count == 0) return NULL;
        Node* max_node = this->maximum(this->root);
        return &(max_node->key);
    }
    void remove_min() {
        if(this->root == NULL) {
            return;
        }
        this->root = this->remove_min(this->root);
    }

    void remove_max() {
        if(this->root == NULL) {
            return;
        }
        this->root = this->remove_max(this->root);
    }
    void remove(Key key) {
        this->root = remove(this->root, key);
    }

3.6 數值運算:floorceil

floorceil分別是地板和天花板的意思。在一個數組中,對於指定元素n,如果數組中存在n,那么n的兩個值就是它本身;如果不存在,那么分別是距離最近的小於指定元素的值 和 距離最近的大於指定元素的值。

private:
    Node* floor(Node* node, Key key) {
        if(node == NULL) {
            return NULL;
        }

//        key等於node->key:floor的結果就是node本身
        if(node->key == key) {
            return node;
        }

//        key小於node—>key:floor的結果肯定在node節點的左子樹
        if(node->key > key) {
            return floor(node->left, key);
        }

//        key大於node->key:右子樹可能存在比node->key大,但是比key小的節點
//        如果存在上述情況,返回這個被選出來的節點
//        否則,函數最后返回node本身
        Node* tmp = floor(node->right, key);
        if(tmp != NULL) {
            return tmp;
        }

        return node;
    }

    Node* ceil(Node* node, Key key) {
        if(node == NULL) {
            return NULL;
        }
        if(node->key == key) {
            return node;
        }

        if(node->key < key) {
            return ceil(node->right, key);
        }

        Node* tmp = ceil(node->left, key);
        if(tmp != NULL) {
            return tmp;
        }

        return node;
    }
public:
    Key* floor(Key key) {
        Key* min_key = this->minimum();
        if(this->isEmpty() || key < *min_key) {
            return NULL;
        }
//        floor node
        Node *node = floor(this->root, key);
        return &(node->key);
    }

    Key* ceil(Key key) {
        Key* max_key = this->maximum();
        if(this->isEmpty() || key > *max_key) {
            return NULL;
        }
//        ceil node
        Node* node = ceil(this->root, key);
        return &(node->key);
    }

4. 代碼測試

第 3 部分實現的測試代碼地址:https://gist.github.com/dongyuanxin/759d16e1ce87913ad2f359d49d5f5016

這是 Github 的 GIST,請自備梯子。

5. 拓展延伸

考慮一種數據類型,如果是基本有序的一組數據,一次insert進入二叉搜索樹,那么,二叉搜索樹就退化為了鏈表。此時,上述所有操作的時間復雜度都會退化為 \(O(log_2 N)\)

為了避免這種情況,就有了紅黑樹等數據結構,來保證樹的平衡性:左右子樹的高度差小於等於 1。

6. 致謝

本篇博客是總結於慕課網的《學習算法思想 修煉編程內功》的筆記,強推強推強推。

二分搜索樹的刪除節點操作


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM