作者按:因為教程所示圖片使用的是 github 倉庫圖片,網速過慢的朋友請移步《二叉搜索樹的實現與常見用法》原文地址。更歡迎來我的小站看更多原創內容:godbmw.com,進行“姿勢”交流 ♪(^∇^*)
1. 為什么需要二叉搜索樹?
選擇數據結構的核心在於解決問題,而不是為了使用而使用。
由於二叉搜索樹的定義和特性,它可以高效解決以下問題:
- 查找問題:二分查找
- 高級結構:字典結構實現
- 數據變動:節點的插入、刪除
- 遍歷問題:前序、中序、后序和層次遍歷
- 數值運算:
ceil
、floor
、找到第 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 實現節點插入
插入采取遞歸的寫法,思路如下:
- 遞歸到底層情況:新建節點,並且返回
- 非底層情況:如果當前鍵等於插入鍵,則更新當前節點的值;小於,進入當前節點的左子樹;大於,進入當前節點的右子樹。
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 個函數:contain
和search
。前者返回布爾型,表示樹中是否有這個節點;后者返回指針類型,表示樹中節點對應的值。
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
這種數據結構。思路如下:
- 首先,將根節點放入隊列
- 如果隊列不空,進入循環
- 取出隊列頭部元素,輸出信息。並將這個元素出隊
- 將這個元素非空的左右節點依次放入隊列
- 檢測隊列是否為空,不空的進入第 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 實現節點刪除
為了方便實現,首先封裝了獲取最小鍵值和最大鍵值的兩個方法:minimum
和maximum
。
刪除節點的原理很簡單(忘了什么名字,是一個計算機科學家提出的),思路如下:
- 如果左節點為空,刪除本節點,返回右節點。
- 如果右節點為空,刪除本節點,返回左節點。
- 如果左右節點都為空,是 1 或者 2 的子情況。
- 如果左右節點都不為空,找到當前節點的右子樹的最小節點,並用這個最小節點替換本節點。
為什么第 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 數值運算:floor
和ceil
floor
和ceil
分別是地板和天花板的意思。在一個數組中,對於指定元素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. 致謝
本篇博客是總結於慕課網的《學習算法思想 修煉編程內功》的筆記,強推強推強推。