4. 二分搜尋樹
阿新 • • 發佈:2018-12-01
一. 二分查詢法
- 對於有序數列,才能使用二分查詢法(排序的作用)
- 遞迴實現通常思維起來更容易, 但效能不如 while迴圈法
程式碼
#include <iostream> #include <cassert> #include <ctime> using namespace std; // 二分查詢法,在有序陣列arr中,查詢target // 如果找到target,返回相應的索引index // 如果沒有找到target,返回-1 template<typename T> int binarySearch(T arr[], int n, T target){ // 在arr[l...r]之中查詢target int l = 0, r = n-1; while( l <= r ){ //int mid = (l + r)/2; // 防止極端情況下的整形溢位,使用下面的邏輯求出mid int mid = l + (r-l)/2; if( arr[mid] == target ) return mid; if( arr[mid] > target ) r = mid - 1; else l = mid + 1; } return -1; } // 用遞迴的方式寫二分查詢法 template<typename T> int __binarySearch2(T arr[], int l, int r, T target){ if( l > r ) return -1; //int mid = (l+r)/2; // 防止極端情況下的整形溢位,使用下面的邏輯求出mid int mid = l + (r-l)/2; if( arr[mid] == target ) return mid; else if( arr[mid] > target ) return __binarySearch2(arr, l, mid-1, target); else return __binarySearch2(arr, mid+1, r, target); } template<typename T> int binarySearch2(T arr[], int n, T target){ return __binarySearch2( arr , 0 , n-1, target); } // 比較非遞迴和遞迴寫法的二分查詢的效率 // 非遞迴演算法在效能上有微弱優勢 int main() { int n = 1000000; int* a = new int[n]; for( int i = 0 ; i < n ; i ++ ) a[i] = i; // 測試非遞迴二分查詢法 clock_t startTime = clock(); // 對於我們的待查詢陣列[0...N) // 對[0...N)區間的數值使用二分查詢,最終結果應該就是數字本身 // 對[N...2*N)區間的數值使用二分查詢,因為這些數字不在arr中,結果為-1 for( int i = 0 ; i < 2*n ; i ++ ){ int v = binarySearch(a, n, i); if( i < n ) assert( v == i ); else assert( v == -1 ); } clock_t endTime = clock(); cout << "Binary Search (Without Recursion): " << double(endTime - startTime) / CLOCKS_PER_SEC << " s"<<endl; // 測試遞迴的二分查詢法 startTime = clock(); // 對於我們的待查詢陣列[0...N) // 對[0...N)區間的數值使用二分查詢,最終結果應該就是數字本身 // 對[N...2*N)區間的數值使用二分查詢,因為這些數字不在arr中,結果為-1 for( int i = 0 ; i < 2*n ; i ++ ){ int v = binarySearch2(a, n, i); if( i < n ) assert( v == i ); else assert( v == -1 ); } endTime = clock(); cout << "Binary Search (Recursion): " << double(endTime - startTime) / CLOCKS_PER_SEC << " s"<<endl; delete[] a; return 0; }
二. 二分搜尋樹基礎 (Binary Search Tree)
二分搜尋數的優勢
- 體現在 實現類似 python中的 dict 資料結構 key-value
- 高效
不僅可以查詢資料; 還可以高效地插入, 刪除資料-動態維護資料
可以方面的回答很多資料之間的關係問題
min, max, floor, ceil, rank, select
特點
二分搜尋數的基礎結構編寫
main.cpp
#include <iostream> 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 = this->right = NULL; } }; Node *root; // 根節點 int count; // 節點個數 public: // 建構函式, 預設構造一棵空二分搜尋樹 BST(){ root = NULL; count = 0; } ~BST(){ // TODO: ~BST() 析構 比較複雜, 後面講解 } // 返回二分搜尋樹的節點個數 int size(){ return count; } // 返回二分搜尋樹是否為空 bool isEmpty(){ return count == 0; } }; int main() { return 0; }
三. 二分搜尋數的節點插入
- 從根節點開始尋找合適的位置
- 找到滿足 左子節點 < 父節點 < 右子節點
程式碼 main.cpp
class BST{ ... private: ... // 向二分搜尋樹中插入一個新的(key, value)資料對 void insert(Key key, Value value){ root = insert(root, key, value); } private: // 向以node為根的二分搜尋樹中, 插入節點(key, value), 使用遞迴演算法 // 返回插入新節點後的二分搜尋樹的根 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 // key > node->key node->right = insert( node->right, key, value); return node; } }; ...
四. 二分搜尋樹的查詢
- 查詢的邏輯和insert 類似
- 實現 contain(是否包含) 和 search(查詢key對應的value)
程式碼 main.cpp
// 二分搜尋樹
template <typename Key, typename Value>
class BST{
...
public:
...
// 檢視二分搜尋樹中是否存在鍵key
bool contain(Key key){
return contain(root, key);
}
// 在二分搜尋樹中搜索鍵key所對應的值。如果這個值不存在, 則返回NULL
Value* search(Key key){
return search( root , key );
}
private:
...
// 檢視以node為根的二分搜尋樹中是否包含鍵值為key的節點, 使用遞迴演算法
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 // key > node->key
return contain( node->right , key );
}
// 在以node為根的二分搜尋樹中查詢key所對應的value, 遞迴演算法
// 若value不存在, 則返回NULL
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 // key > node->key
return search( node->right, key );
}
};
五. 二分搜尋樹的遍歷(深度優先遍歷)
二分搜尋樹的 前中後序遍歷
- 前序遍歷: 先訪問當前節點, 再依次遞迴訪問左右子樹
- 中序遍歷(可以實現排序): 先遞迴訪問左子樹, 再訪問當前節點和 遞迴訪問右子樹
- 後序遍歷(可以實現釋放): 先遞迴訪問左右子樹, 再訪問當前節點
程式碼 main.cpp
// 二分搜尋樹
template <typename Key, typename Value>
class BST{
private:
...
// 二分搜尋樹的前序遍歷
void preOrder(){
preOrder(root);
}
// 二分搜尋樹的中序遍歷
void inOrder(){
inOrder(root);
}
// 二分搜尋樹的後序遍歷
void postOrder(){
postOrder(root);
}
private:
...
// 對以node為根的二叉搜尋樹進行前序遍歷, 遞迴演算法
void preOrder(Node* node){
if( node != NULL ){
cout<<node->key<<endl;
preOrder(node->left);
preOrder(node->right);
}
}
// 對以node為根的二叉搜尋樹進行中序遍歷, 遞迴演算法
void inOrder(Node* node){
if( node != NULL ){
inOrder(node->left);
cout<<node->key<<endl;
inOrder(node->right);
}
}
// 對以node為根的二叉搜尋樹進行後序遍歷, 遞迴演算法
void postOrder(Node* node){
if( node != NULL ){
postOrder(node->left);
postOrder(node->right);
cout<<node->key<<endl;
}
}
// 釋放以node為根的二分搜尋樹的所有節點
// 採用後續遍歷的遞迴演算法
void destroy(Node* node){
if( node != NULL ){
destroy( node->left );
destroy( node->right );
delete node;
count --;
}
}
};
// 測試二分搜尋樹的前中後序遍歷
int main() {
srand(time(NULL));
BST<int,int> bst = BST<int,int>();
// 取n個取值範圍在[0...m)的隨機整數放進二分搜尋樹中
int N = 10;
int M = 100;
for( int i = 0 ; i < N ; i ++ ){
int key = rand()%M;
// 為了後續測試方便,這裡value值取和key值一樣
int value = key;
cout<<key<<" ";
bst.insert(key,value);
}
cout<<endl;
// 測試二分搜尋樹的size()
cout<<"size: "<<bst.size()<<endl<<endl;
// 測試二分搜尋樹的前序遍歷 preOrder
cout<<"preOrder: "<<endl;
bst.preOrder();
cout<<endl;
// 測試二分搜尋樹的中序遍歷 inOrder
cout<<"inOrder: "<<endl;
bst.inOrder();
cout<<endl;
// 測試二分搜尋樹的後序遍歷 postOrder
cout<<"postOrder: "<<endl;
bst.postOrder();
cout<<endl;
return 0;
}
六. 層序遍歷(廣度優先遍歷)
程式碼
main.cpp
...
#include <queue>
...
// 二分搜尋樹的層序遍歷
void levelOrder(){
queue<Node*> q;
q.push(root); //
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 );
}
}
...
七. 刪除最大值, 最小值
- 最左的node 即最小值
- 最右邊的node 即 最大值
程式碼 main.cpp
// 尋找二分搜尋樹的最小的鍵值
Key minimum(){
assert( count != 0 );
Node* minNode = minimum( root );
return minNode->key;
}
// 尋找二分搜尋樹的最大的鍵值
Key maximum(){
assert( count != 0 );
Node* maxNode = maximum(root);
return maxNode->key;
}
// 從二分搜尋樹中刪除最小值所在節點
void removeMin(){
if( root )
root = removeMin( root );
}
// 從二分搜尋樹中刪除最大值所在節點
void removeMax(){
if( root )
root = removeMax( root );
}
...
// 返回以node為根的二分搜尋樹的最小鍵值所在的節點
Node* minimum(Node* node){
if( node->left == NULL )
return node;
return minimum(node->left);
}
// 返回以node為根的二分搜尋樹的最大鍵值所在的節點
Node* maximum(Node* node){
if( node->right == NULL )
return node;
return maximum(node->right);
}
// 刪除掉以node為根的二分搜尋樹中的最小節點
// 返回刪除節點後新的二分搜尋樹的根
Node* removeMin(Node* node){
if( node->left == NULL ){
Node* rightNode = node->right;
delete node;
count --;
return rightNode;
}
node->left = removeMin(node->left);
return node;
}
// 刪除掉以node為根的二分搜尋樹中的最大節點
// 返回刪除節點後新的二分搜尋樹的根
Node* removeMax(Node* node){
if( node->right == NULL ){
Node* leftNode = node->left;
// 為什麼node-left 一定比 node的父節點小?
// 因為insert的時候都是從root開始查詢合適的位置的
delete node;
count --;
return leftNode;
}
node->right = removeMax(node->right);
return node;
}
八. 二分搜尋樹節點的刪除
程式碼 main.cpp
// 二分搜尋樹
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 = this->right = NULL;
}
Node(Node *node){
this->key = node->key;
this->value = node->value;
this->left = node->left;
this->right = node->right;
}
};
...
// 從二分搜尋樹中刪除鍵值為key的節點
void remove(Key key){
root = remove(root, key);
}
...
// 刪除掉以node為根的二分搜尋樹中鍵值為key的節點, 遞迴演算法
// 返回刪除節點後新的二分搜尋樹的根
Node* remove(Node* node, Key key){
if( node == NULL )
return NULL;
if( key < node->key ){
node->left = remove( node->left , key );
return node;
}
else if( key > node->key ){
node->right = remove( node->right, key );
return node;
}
else{ // key == node->key
// 待刪除節點左子樹為空的情況
if( node->left == NULL ){
Node *rightNode = node->right;
delete node;
count --;
return rightNode;
}
// 待刪除節點右子樹為空的情況
if( node->right == NULL ){
Node *leftNode = node->left;
delete node;
count--;
return leftNode;
}
// 待刪除節點左右子樹均不為空的情況
// 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
// 用這個節點頂替待刪除節點的位置
Node *successor = new Node(minimum(node->right));
count ++;
successor->right = removeMin(node->right);
successor->left = node->left;
delete node;
count --;
return successor;
}
}
補充說明
- 我們除了使用 s = min(d->right) 代替被刪除節點d,
- 也可以用 p = max(d->left) 代替被刪除節點d
九. 二分搜尋樹的侷限性
如果二分搜尋樹 極不平衡, 會導致演算法從O(logn)退化為O(n)級別
極不平衡的二分搜尋樹
1
\
2
\
3
\
4
\
5