二叉搜索樹
顧名思義,一棵二叉搜索樹是以一棵二叉樹來組織的。如下圖所示,這樣一棵樹可以使用一個鏈表數(shù)據(jù)結(jié)構(gòu)來表示,其中每個結(jié)點就是一個對象,除了key外,每個結(jié)點還包括屬性left 、right、和p,它們分別指向結(jié)點的左孩子、右孩子和雙親。如果某個孩子結(jié)點和父結(jié)點不存在,則相應(yīng)屬性的值為nil,根結(jié)點是樹中唯一父指針為nil的結(jié)點
二叉搜索樹中的關(guān)鍵字總是以滿足二叉搜索樹性質(zhì)的方式來存儲:
設(shè)x是二叉搜索樹中的一個結(jié)點,如果y是x的左子樹中的一個結(jié)點,那么y.key
x.key。如果y是x的右子樹中的一個結(jié)點,那么y.key
x.key
如圖(a)中,樹根的關(guān)鍵字是6,在其左子樹中有關(guān)鍵字2、5和5,它們均不大于6;而其右子樹中有關(guān)鍵字7和8,它們均不小于6。所以這個性質(zhì)在樹中的每個結(jié)點都成立。
二叉搜索樹的性質(zhì)允許我們通過一個簡單的遞歸算法來按序輸出二叉搜索樹中的所有關(guān)鍵字,算法是我們熟悉的中序遍歷
typedef struct Node
{
struct Node *p;
struct Node *left;
struct Node *right;
int key;
} Node;
void InorderTreeWalk(Node *x) // 中序遍歷樹x
{
if (x != nullptr) {
InorderTreeWalk(x->left); // 遞歸左孩子
cout<<x->key<<endl; // 輸出根節(jié)點關(guān)鍵字
InorderTreeWalk(x->right); // 遞歸右孩子
}
}
遍歷一棵有n個結(jié)點的二叉搜索樹,需要耗費O(n)的時間,因為初次調(diào)用后,對于樹中的每個結(jié)點這個過程恰好要自己調(diào)用兩次;一次是它的左孩子,另一次是它的右孩子
查詢二叉搜索樹
我們經(jīng)常需要查找一個存儲在二叉搜索樹中的關(guān)鍵字,下面將討論并且說明在任何高度為h的二叉搜索樹上,如何在O(h)時間內(nèi)完成每個操作
查找
使用下面的代碼在一棵二叉搜索樹中查找一個具有給定關(guān)鍵字的結(jié)點,輸入一個指向樹根的指針和一個關(guān)鍵字k,如果這個節(jié)點存在,那么返回關(guān)鍵字為k的指針,否則為nil
Node * TreeSearch(Node *x, int k)
{
if (x == nullptr || k == x->key) {
return x;
}
if (k < x->key)
{
return TreeSearch(x->left, k);
}
else
{
return TreeSearch(x->right, k);
}
}
這個過程是從樹根開始查找的,并沿著這顆樹中的一條簡單路徑向下進行。對于遇到的每個結(jié)點x,比較關(guān)鍵字k與x.key,如果兩個關(guān)鍵字相等,查找就終止了,該結(jié)點就是我們需要查找的結(jié)點。
如果k小于x.key,則查找x的左子樹,因為二叉搜索樹的性質(zhì)告訴了我們k不可能在右子樹中。相應(yīng)的,如果k大于x.key,查找在右子樹進行。
從樹根開始遞歸期間遇到的結(jié)點就形成了一條向下的簡單路徑,所以TreeSearch的運行時間是O(h),其中h是樹的高度
除了遞歸方式,采用迭代方式是效率更高的方式
Node * InteractiveTreeSearch(Node *x, int k)
{
while (x != nullptr && k != x->key) {
if (k < x->key)
{
x = x->left;
}
else
{
x = x->right;
}
}
return x;
}
最大關(guān)鍵字元素和最小關(guān)鍵字元素
通過從樹根開始沿著left孩子指針直到遇到一個nil,
- 最小關(guān)鍵字元素
Node *TreeMinimum(Node *x)
{
while (x->left != nullptr) {
x = x->left;
}
return x;
}
二叉樹的性質(zhì)保證了上面查找的正確性。
如果結(jié)點x沒有左子樹,那么由于x的右子樹中的每個關(guān)鍵字都至少大于或等于x.key,則以x為根的子樹中的最小關(guān)鍵字是x.key。如果結(jié)點x有左子樹,那么由于其右子樹中沒有關(guān)鍵字小于x.key,且在左子樹中的每個關(guān)鍵字不大于x.key,則以x為根的子樹中的最小關(guān)鍵字一定在以x.left為根的子樹中
- 最大關(guān)鍵字元素
最大元素一定是沿著right孩子指針不斷向下查找,直到遇到第一個空指針;
Node *TreeMaxmum(Node *x)
{
while (x->right != nullptr) {
x = x->right;
}
return x;
}
這兩個過程在一棵高度為h的樹上均能在O(h)的時間內(nèi)執(zhí)行完。
后繼和前驅(qū)
給定一棵二叉搜索樹,有時候需要按中序遍歷的次序查找它的后繼。如果所有的關(guān)鍵字互不相同,則一個結(jié)點x的后繼是大于x.key的最小關(guān)鍵字的結(jié)點
后繼
Node *TreeSuccessor(Node *T, int t)
{
// 查找結(jié)點
Node *x = TreeSearch(T, t);
Node *y = nullptr;
// 右子樹非空,即查找最小關(guān)鍵字
if (x->right != nullptr) {
y = TreeMinimum(x->right);
return y;
}
// 查找結(jié)點的父指針
y = x->p;
while (y != nullptr && x == y->right) {
x = y;
y = y->p;
}
return y;
}
如果結(jié)點x的右子樹非空,那么x的后繼恰好是x右子樹中的最左結(jié)點。也可以理解為最小關(guān)鍵字
如果結(jié)點x的右子樹為空并有一個后繼y,那么y就是x的最底層祖先,并且y的左孩子也是x的一個祖先(比如:上圖中關(guān)鍵字為13的結(jié)點的后繼是關(guān)鍵字為15的結(jié)點)。為了找到y(tǒng),只需要簡單地從x開始沿樹而上直到遇到這樣一個結(jié)點:這個結(jié)點是它的雙親的左孩子
前驅(qū)
Node *TreePredecessor(Node *T, int t)
{
// 查找元素t
Node *x = TreeSearch(T, t);
Node *y = nullptr;
// 左子樹非空
if (x->left != nullptr)
{
return TreeMaxmum(x->left);
}
// 查結(jié)點的父指針
y = x->p;
while ( y != nullptr && x == y->left) {
x = y;
y = y->p;
}
return y;
}
對于樹的后繼和前驅(qū)的查找,運行時間為O(h)
插入和刪除
插入和刪除操作會引起由二叉搜索樹表示的動態(tài)集合的變化。所以一定要修改數(shù)據(jù)結(jié)構(gòu)來反映這個變化,但修改要保持二叉搜索樹性質(zhì)的成立
插入
向一個二叉搜索樹中插入一個x結(jié)點,只需要不斷比較x.key與當(dāng)前結(jié)點z.key的大小,若小于,則肯定是向z的左子樹插入,否則向z的右子樹插入,循環(huán)比較,直到遇到當(dāng)前結(jié)點的左/右子樹為空為止。這時候已經(jīng)找到了要插入的結(jié)點的父結(jié)點的位置,最后判斷是左子樹還是右子樹即可
Node * TreeInsert(Node *root, Node *z)
{
Node *y = nullptr;
Node *x = root;
// 節(jié)點非空,看是左子樹還是右子樹插入
while (x != nullptr) {
y = x;
if (z->key < x->key)
{
x = x->left;
}
else
{
x = x->right;
}
}
z->p = y;
if (y == nullptr) // 建立第一個節(jié)點
{
root = z;
}
else if (z->key < y->key) // 小于父結(jié)點即位左子樹
{
y->left = z;
}
else // 大于即為右子樹
{
y->right = z;
}
return root;
}
比如:上圖插入結(jié)點,關(guān)鍵字為13,可以對照代碼和圖形理解
刪除
從一棵二叉搜索樹T中刪除一個結(jié)點z,存在3種情況:
1、如果z沒有孩子結(jié)點,那么只是簡單地將它刪除,并修改父結(jié)點,用nil作為孩子來替換z
2、如果z只有一個孩子,那么將孩子提升到樹中z的位置,并修改z的父結(jié)點,用z的孩子來替換z
3、如果z有兩個孩子,那么找z的后繼y(一定在z的右子樹中),并讓y占據(jù)樹中z的位置。z的原來右子樹部分成為y的新的右子樹,并且z的左子樹成為y的新的左子樹
對照圖來理解上面幾種情況
- 如果z沒有左孩子(如下圖),那么用其右孩子來替換z,這個右孩子可以是nil,也可以不是nil。當(dāng)右孩子是nil的時候,此時就是z沒有孩子結(jié)點的情形。當(dāng)右孩子非nil時,這就是z僅有一個孩子的情況,該孩子是其右孩子
- 如果z僅有一個孩子且為左孩子(如下圖),那么用其左孩子替換z
- z既有左孩子也有右孩子。如果y是z的右孩子(如下圖),那么用y替換z,并僅留下y的右孩子
- 如果y位于z的右子樹中但并不是z的右孩子(如下圖),這種情況下,先用y的右孩子替換y,然后再用y替換z
在二叉搜索樹內(nèi)移動子樹,它是用另一棵子樹替換一棵子樹并成為其雙親的孩子結(jié)點
移動子樹
void Transplant(Node **T, Node *u, Node *v) // 用結(jié)點v替換結(jié)點u
{
if (u->p == nullptr) // 父結(jié)點不存在,那么u為根結(jié)點
{
*T = v;
}
else if (u == u->p->left) // u為左孩子,那么將v作為左孩子
{
u->p->left = v;
}
else // u為右孩子,那么將v作為右孩子
{
u->p->right = v;
}
if (v != nullptr) // v不為nil,那么更新父結(jié)點
{
v->p = u->p;
}
}
刪除結(jié)點
Node * TreeDelete(Node *T, int k)
{
Node *z = TreeSearch(T, k); // 查找結(jié)點z
Node *y = nullptr;
if (z->left == nullptr) // 結(jié)點z沒有左孩子
{
Transplant(&T, z, z->right);
}
else if (z->right == nullptr) // 結(jié)點z沒有右孩子
{
Transplant(&T, z, z->left);
}
else // 結(jié)點z有兩個孩子
{
y = TreeMinimum(z->right); // 找右子樹的最小結(jié)點
if (y->p != z) { // y的父結(jié)點不是z
Transplant(&T, y, y->right);
y->right = z->right;
y->right->p = y;
}
Transplant(&T, z, y);
y->left = z->left;
y->left->p = y;
}
return T;
}
簡單使用
// 創(chuàng)建搜索二叉樹
Node * TreeEstablish(int *a, int length)
{
Node *root = nullptr;
for (int i=0 ; i < length; i++) {
Node *node = (Node *)malloc(sizeof(Node));
node->key = a[i];
node->p = nullptr;
node->left = nullptr;
node->right = nullptr;
root = TreeInsert(root, node);
}
return root;
}
int main(int argc, const char * argv[]) {
int a[] = {2,3,4,6,15,7,18,17,20,13,9};
int length = sizeof(a)/sizeof(a[0]);
Node *T;
T = TreeEstablish(a, length); // 建立二叉搜索樹
InorderTreeWalk(T); // 中序遍歷打印
cout<<"6 successor is "<<TreeSuccessor(T, 6)->key<<endl; // 后繼查找
cout<<"18 predecessor is "<<TreePredecessor(T, 18)->key<<endl; // 前驅(qū)查找
T = TreeDelete(T, 2); // 刪除結(jié)點
TreeInorderPrint(T); // 刪除結(jié)點之后的二叉搜索樹
return 0;
}
參考
《算法導(dǎo)論》