二叉搜索樹
二叉搜索樹用來解決計算機中非常重要的基礎問題——查找問題。
二分查找法
對于有序數列才能使用二分查找法。
// 二分查找法,在有序數組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);
}
// 比較非遞歸和遞歸寫法的二分查找的效率
// 非遞歸算法在性能上有微弱優勢
二分查找法的變種floor與ceil
當有序數組中存在大量重復鍵值時,
查找值的下限與上限。
查找v
// 二分查找法, 在有序數組arr中, 查找target
// 如果找到target, 返回第一個target相應的索引index
// 如果沒有找到target, 返回比target小的最大值相應的索引, 如果這個最大值有多個, 返回最大索引
// 如果這個target比整個數組的最小元素值還要小, 則不存在這個target的floor值, 返回-1
template<typename T>
int floor(T arr[], int n, T target){
assert( n >= 0 );
// 尋找比target小的最大索引
int l = -1, r = n-1; //l取-1是為了標記當數組中沒有target值時的下限
while( l < r ){
// 使用向上取整避免死循環
int mid = l + (r-l+1)/2;
if( arr[mid] >= target )
r = mid - 1;
else
l = mid;
}
assert( l == r );
// 如果該索引+1就是target本身, 該索引+1即為返回值
//當數組中存在target值,此時的l是target值閉區間左邊的一個值
if( l + 1 < n && arr[l+1] == target )
return l + 1;
// 否則, 該索引即為返回值
return l;
}
// 二分查找法, 在有序數組arr中, 查找target
// 如果找到target, 返回最后一個target相應的索引index
// 如果沒有找到target, 返回比target大的最小值相應的索引, 如果這個最小值有多個, 返回最小的索引
// 如果這個target比整個數組的最大元素值還要大, 則不存在這個target的ceil值, 返回整個數組元素個數n
template<typename T>
int ceil(T arr[], int n, T target){
assert( n >= 0 );
// 尋找比target大的最小索引值
int l = 0, r = n; //r取n的是為了標記數組中沒有target值的上限
while( l < r ){
// 使用普通的向下取整即可避免死循環
int mid = l + (r-l)/2;
if( arr[mid] <= target )
l = mid + 1;
else // arr[mid] > target
r = mid;
}
assert( l == r );
// 如果該索引-1就是target本身, 該索引+1即為返回值
//當數組中存在target值,此時的r是target值閉區間右邊的一個值
if( r - 1 >= 0 && arr[r-1] == target )
return r-1;
// 否則, 該索引即為返回值
return r;
}
二分搜索樹
二分搜索樹的優勢:
查找表的實現,字典數據結構
不僅可查找數據,還可以高效地插入、刪除數據-動態維護數據
可以方便地回答很多數據之間的關系問題:
min,max,floor(最接近某個結點的下限結點),ceil(最接近某個結點的上限結點),rank(某個結點在樹中排名第幾),select(找出排名為X的元素)
二分搜索樹不一定是完全二叉樹
以下的二叉搜索樹樹不支持重復元素。
若要支持的話可以設定結點左子樹為小于等于該結點的子樹,但這樣會讓樹過大。那比較可行的方法是為結構體添加一個新的屬性count來表示此結點的個數。
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;
}
};
Node *root; // 根節點
int count; // 樹中的節點個數
public:
// 構造函數, 默認構造一棵空二分搜索樹
BST(){
root = NULL;
count = 0;
}
// 析構函數, 釋放二分搜索樹的所有空間
~BST(){
destroy( root );
}
// 返回二分搜索樹的節點個數
int size(){
return count;
}
// 返回二分搜索樹是否為空
bool isEmpty(){
return count == 0;
}
// 向二分搜索樹中插入一個新的(key, value)數據對
void insert(Key key, Value value){
root = insert(root, key, value);
}
// 查看二分搜索樹中是否存在鍵key
bool contain(Key key){
return contain(root, key);
}
// 在二分搜索樹中搜索鍵key所對應的值。如果這個值不存在, 則返回NULL
Value* search(Key key){
return search( root , key );
}
// 二分搜索樹的前序遍歷
void preOrder(){
preOrder(root);
}
// 二分搜索樹的中序遍歷
void inOrder(){
inOrder(root);
}
// 二分搜索樹的后序遍歷
void postOrder(){
postOrder(root);
}
// 二分搜索樹的層序遍歷
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 );
}
}
// 尋找二分搜索樹的最小的鍵值
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 );
}
// 從二分搜索樹中刪除鍵值為key的節點
void remove(Key key){
root = remove(root, key);
}
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;
}
// 查看以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 );
}
// 對以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 --;
}
}
// 返回以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;
delete node;
count --;
return leftNode;
}
node->right = removeMax(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 );
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->left != NULL && node->right != NULL
Node *successor = new Node(minimum(node->right));
count ++;
successor->right = removeMin(node->right);
successor->left = node->left;
delete node;
count --;
return successor;
}
}
};
二叉搜索樹的局限性
當序列1、2、3、4、5、6插入二叉搜索樹時,它是退化成鏈表的
解決方案是:平衡二叉樹,其中一種實現是——紅黑樹
其它的實現是:2-3 tree、AVL tree、Splay tree
課外話題:一種平衡二叉樹和堆結合的數據結構——Treap。既保持了二叉樹的性質又能進行堆那樣具有優先級的操作。
二叉搜索樹的floor與ceil
// 在以node為根的二叉搜索樹中, 尋找key的floor值所處的節點, 遞歸算法
Node* floor(Node* node, Key key){
if( node == NULL )
return NULL;
// 如果node的key值和要尋找的key值相等
// 則node本身就是key的floor節點
if( node->key == key )
return node;
// 如果node的key值比要尋找的key值大
// 則要尋找的key的floor節點一定在node的左子樹中
if( node->key > key )
return floor( node->left , key );
// 如果node->key < key
// 則node有可能是key的floor節點, 也有可能不是(存在比node->key大但是小于key的其余節點)
// 需要嘗試向node的右子樹尋找一下
Node* tempNode = floor( node->right , key );
if( tempNode != NULL )
return tempNode;
return node;
}
// 在以node為根的二叉搜索樹中, 尋找key的ceil值所處的節點, 遞歸算法
Node* ceil(Node* node, Key key){
if( node == NULL )
return NULL;
// 如果node的key值和要尋找的key值相等
// 則node本身就是key的ceil節點
if( node->key == key )
return node;
// 如果node的key值比要尋找的key值小
// 則要尋找的key的ceil節點一定在node的右子樹中
if( node->key < key )
return ceil( node->right , key );
// 如果node->key > key
// 則node有可能是key的ceil節點, 也有可能不是(存在比node->key小但是大于key的其余節點)
// 需要嘗試向node的左子樹尋找一下
Node* tempNode = ceil( node->left , key );
if( tempNode != NULL )
return tempNode;
return node;
}
};
二叉搜索樹的successor與presuccessor
Key* predecessor(Key key){
Node *node = search(root, key);
// 如果key所在的節點不存在, 則key沒有前驅, 返回NULL
if(node == NULL)
return NULL;
// 如果key所在的節點左子樹不為空,則其左子樹的最大值為key的前驅
if(node->left != NULL)
return &(maximum(node->left)->key);
// 否則, key的前驅在從根節點到key的路徑上, 在這個路徑上尋找到比key小的最大值, 即為key的前驅
Node* preNode = predecessorFromAncestor(root, key);
return preNode == NULL ? NULL : &(preNode->key);
}
// 查找key的后繼, 遞歸算法
// 如果不存在key的后繼(key不存在, 或者key是整棵二叉樹中的最大值), 則返回NULL
Key* successor(Key key){
Node *node = search(root, key);
// 如果key所在的節點不存在, 則key沒有前驅, 返回NULL
if(node == NULL)
return NULL;
// 如果key所在的節點右子樹不為空,則其右子樹的最小值為key的后繼
if(node->right != NULL)
return &(minimum(node->right)->key);
// 否則, key的后繼在從根節點到key的路徑上, 在這個路徑上尋找到比key大的最小值, 即為key的后繼
Node* sucNode = successorFromAncestor(root, key);
return sucNode == NULL ? NULL : &(sucNode->key);
}