二叉搜尋樹
注意:本文的演算法和程式碼思路大部分來自《演算法導論》
什麼是二叉搜尋樹
二叉搜尋樹首先是一棵二叉樹,此外,它還能用來搜尋。因為它滿足這樣的性質:每個結點的左子樹的結點值都比自身小,而它的右子樹的結點值都比自身大。
它長得像下面這樣:(依據建立時結點插入順序不同,可能是滿二叉,也可能不是)
二叉搜尋樹可以非常方便的用來進行查詢指定元素,查詢最大值和最小值等。
定義資料結構
我們可以用連結串列或者陣列的方式來實現一棵樹。這裡我們採用連結串列的方式。
首先定義節點結構,可以看到,每個結點有一個值,並且有兩個孩子,並且有一個父親。
此外,我們還要額外定義一棵樹的結構,它很簡單,只有一個指向樹根的指標。
//樹結點結構
struct Node {
int key;
Node* left;
Node* right;
Node* parent;
};
//樹
struct Tree {
Node* root;
};
二叉樹的遍歷
因為二叉搜尋樹的特殊性質,我們對其進行中序遍歷就可以得到所有元素的一個有序序列。
遍歷既可以用遞迴也可以用迭代的方式去實現,這裡給出遞迴的版本:
//中序遍歷
void inorder_tree_walk(Node* x) {
if (x != NULL) {
inorder_tree_walk(x->left);
cout << x->key << " ";
inorder_tree_walk(x->right);
}
}
//前序遍歷
void preorder_tree_walk(Node* x) {
if (x != NULL) {
cout << x->key << " ";
preorder_tree_walk(x->left);
preorder_tree_walk(x->right);
}
}
//後續遍歷
void postorder_tree_walk(Node* x) {
if (x != NULL) {
postorder_tree_walk(x->left);
postorder_tree_walk(x->right);
cout << x->key << " ";
}
}
遍歷二叉樹的時間複雜度是:O(n)。
如對上圖左邊的那棵樹進行中序遍歷的話,得到的序列就是:2 3 4 5 7 9
查詢指定結點值
如果給出一個關鍵字,想要查詢其是否存在與二叉樹中,如果存在則返回指向它的結點的指標。這個過程可以描述如下:首先把關鍵字和根結點的值做比較,如果相等則返回;否則,如果比根節點值小,那就遞迴的在左子樹中查詢,否則就在右子樹中查詢。
這個過程既可以用遞迴去實現,也可以用迭代去實現,這裡給出兩個版本:
//遞迴版
Node* tree_search(Node* x, int k) {
//找到或者為空
if (x == NULL || k == x->key) {
return x;
}
if (k < x->key) {
return tree_search(x->left, k);
}
else {
return tree_search(x->right, k);
}
}
//迭代版
Node* iterative_tree_search(Node* x, int k) {
while (x != NULL && k != x->key) {
if (k < x->key) {
x = x->left;
}
else {
x = x->right;
}
}
return x;
}
最大元素和最小元素
根據二叉搜尋樹的性質,左子樹的結點值都比其父節點的小,而右子樹的相反。所以,只要從樹根開始,沿著左孩子進行查詢,直到最後一個左孩子,那它肯定就是最小值。最大值也是類似。
像這裡:
這裡給出迭代方式的實現:
//找最小結點
Node* tree_minimum(Node* x) {
while (x->left != NULL) {
x = x->left;
}
return x;
}
//找最大結點
Node* tree_maximum(Node* x) {
while (x->right != NULL) {
x = x->right;
}
return x;
}
前驅和後繼
一個結點的前驅和後繼是什麼呢?它是按照中序遍歷時,排在該結點前和後的第一個結點。
如:上圖的中序遍歷是:2 3 4 5 7 9 , 那5的前驅就是4,而其後繼就是7。
也就是,前驅是剛好比它小(或者等於)的元素,後繼是剛好比它大(或者等於)的元素。
那要怎麼找呢?
首先看這幅圖:
我們先討論,有左子樹的結點的前驅,和有右子樹的結點的後繼。
首先,有左子樹的結點的前驅。我們知道,一個節點的左子樹的值都比他自身小,所以它的前驅肯定是在左子樹中,而且是左子樹中最大的那一個。比如說,結點6,它的前驅就是左子樹中最大的那個,也就是4。
然後,是有右子樹的結點的後繼。很顯然,它應該是它的右子樹中最小的那。比如說,結點
6,它的後繼就是右子樹中最小的那個,也就是7。
那麼,為什麼有左子樹的前驅一定在左子樹中,而不可能在它的父系結點或其他地方呢?
我們可以這樣考慮,看到結點13,它有一個左子樹,左子樹的結點值都比它小。它有一個父親7,且它是它父親的右孩子,所以父親也比它小。那有沒有可能,父親的某一個取值會使得它是13的前驅呢?答案是不可能的。因為前驅是比它小之中的最大的那個,而如果它有左子樹,那左子樹中的元素因為在它13的父親結點的右子樹中,所以肯定比父親結點7要大,但是卻比13小。
接下來是,沒有左子樹的結點的前驅,和沒有右子樹的結點的後繼。
顯然,沒有左子樹的結點的前驅不可能在左子樹裡找,只能在其他地方找。注意到,前驅和後繼其實是對稱的關係,如果b的前驅是a,那麼a的後繼肯定就是b。所以我們要找到a的後繼,相當於要找到b的前驅。比如說我們要找到結點7的前驅,那如果能找到某個結點,它的後繼是7,那就完事了。因為7沒有左子樹,所以7的前驅肯定在父親結點上面。而因為6的後繼就是7,所以6就是7的前驅(可以這樣驗證,因為6有右子樹,且7是右子樹中最小的那個,所以7是6的後繼)。所以這個前驅節點a滿足這樣一個性質:它肯定在結點b的父系結點上,並且,它是第一個使得b在它的右子樹中的結點。
相應的,沒有右子樹的結點的後繼也是類似求法。
這裡給出實現方法:
//前驅
Node* tree_predecessor(Node* x) {
//如果左子樹非空,則前驅是左子樹中最大的結點
if (x->left != NULL) {
return tree_maximum(x->left);
}
//否則,找到父系結點中第一個使得它是其右子孫的結點
Node* y = x->parent;
while (y != NULL && y->left == x) {
x = y;
y = x->parent;
}
return y;
}
//後繼
Node* tree_successor(Node* x) {
//如果右子樹非空,則後繼是右子樹中的最左結點
if (x->right != NULL) {
return tree_minimum(x->right);
}
//否則,找到父系結點中第一個使得它是其左子孫的結點
Node* y = x->parent;
while (y != NULL && y->right == x) {
x = y;
y = x->parent;
}
return y;
}
插入
首先,要明確一點,新結點肯定是以葉節點的形式插入的。而我們要找的就是那個能收養它的父結點。
比如說,我們要在這棵樹裡插入結點8:
首先,8和5比較,比5大,在右子樹中查詢。然後和9比較,比9小;最後和7比較,比7大,但因為7已經沒有右子樹了,所以就把8掛在7的右子樹上。
實現如下:
void tree_insert(Tree* T, Node* z) {
Node* y = NULL; //用來記住父節點
Node* x = T->root; //從根開始查詢
while (x != NULL) {
y = x; //記住要掛留的父節點
if (z->key < x->key) { //在左子樹中找
x = x->left;
}
else {
x = x->right; //在右子樹中找
}
}
z->parent = y; //掛上去
if (y == NULL) { //如果樹是空的
T->root = z;
}
//父親收養它
else if (z->key < y->key) {
y->left = z;
}
else {
y->right = z;
}
}
刪除
刪除是一件比較麻煩的事。我們分3種大的情況來討論。
- 如果z沒有孩子結點,那麼只是簡單的把它刪除掉,並且修改它的父節點指向空。
-
如果z只有一個孩子,那麼將這個孩子提升到樹中z的位置上,並修改z的父節點的孩子指標。
-
如果z有兩個孩子,那麼找到z的後繼y(在右子樹中),並讓y佔據z的位置。
這裡有細分為兩種情況:
-
如果y是z的右孩子,則直接把以y為根的子樹放到z上,在讓z的左子樹成為y的左子樹。
-
如果y不是z的右孩子,則先用y的右孩子來替換y,在用y替換z。
為了完成以上工作,額外定義一個函式transplant,它專門用來移植結點。它用一棵以v為根的子樹來替換一棵以u為根的子樹,結點u的雙親變為結點v的雙親,並且最後v成為u的雙親的相應孩子。
程式碼如下:
void transplant(Tree* T, Node* u, Node* v) { if (u->parent == NULL) { //如果被替換的是樹根,則要讓其成為樹根 T->root = v; } else if (u == u->parent->left) { //如果被替換的那個結點是其父節點的左孩子 u->parent->left = v; } else { //否則是右孩子 u->parent->right = v; } if (v != NULL) { //指向父節點 v->parent = u->parent; } } void tree_delete(Tree* T, Node* z) { //對應第一種和第二種情況 if (z->left == NULL) { transplant(T, z, z->right); } else if (z->right == NULL) { transplant(T, z, z->left); } //對應第三種情況 else { Node* y = tree_minimum(z->right); if (y->parent != z) { //如果y不是z的直接右孩子 transplant(T, y, y->right); y->right = z->right; y->right->parent = y; } transplant(T, z, y); y->left = z->left; y->left->parent = y; } }
-
參考資料: 《演算法導論》Thomas H. Cormen Charles E.Leiserson && Ronald L.Rivest Clifford Stein 著