前言:
學習了一些數據結構之後,你是不是已經有些小得意了,以爲數據結構就這點東西嘛,不就用好棧、隊列什麼什麼的就好,呵呵,那些只是皮毛。接下來的東西纔會讓你真正認識到數據結構的博大精深,當你看到接下來那些酷炫狂賽的操作時,你會驚歎於數據結構的神奇。在學習那些酷炫的AVL樹,紅黑樹之前,我們先入個門,學習最簡單的動態查找表——二叉查找樹。
二叉查找樹:
二叉查找樹有被稱爲二叉排序樹,它要麼是個空樹,要麼就滿足下列條件:
- 若左子樹不空,則左子樹中所有元素的值比根節點小
- 若右子樹不空,則左子樹中所有元素的值比根節點大
- 二叉查找樹的左右子樹都是二叉查找樹(瘋狂暗示遞歸實現)
如圖,便是一個二叉查找樹
基本實現
存儲實現:
二叉查找樹就使用鏈表實現,這樣能夠很好的理解,每個節點有一個元素存儲值,兩個指針分別指向它的左子樹和右子樹。
template<class elemType>
class binarySearchTree
{
private:
struct node{
elemType data;
node *left;
node *right;
node(const elemType &d, node *ln = NULL, node *rn = NULL):data(d), left(ln), right(rn){}
};
node *root;
}
find函數實現:
查找操作從根節點開始,一個一個節點遞歸查找。對於一個特定節點,就四個步驟:
- 如果該節點是NULL,那麼說明已經查找完了整個樹,還沒有找到。
- 若該節點的值與要找的值吻合,那麼找到,退出
- 若要找的值大於該節點的值,查找它的右子樹節點
- 若要找的值小於該節點的值,查找它的左子樹節點
elemType *find(const elemType &x){
return find(x, root);
}
elemType *find(const elemType &x, node *t){
if(t == NULL){
cout << "Can not find" << endl;
return 0;
}
if(t -> data == x){
return &(t -> data);
}
if(x < t -> data){
return find(x, t -> left);
}
else{
return find(x, t -> right);
}
}
midOrder操作:
中序遍歷輸出整個樹的值,細心的人其實已經從二叉查找樹做小右大的性質想到了,二叉查找樹中序遍歷的結果必然是一個從小到大序列啊。
void midOrder(){
midOrder(root);
}
void midOrder(node *p){
if(p == NULL){
return;
}
midOrder(p -> left);
cout << p -> data << ' ';
midOrder(p -> right);
}
insert操作:
insert操作插入的一定是葉節點
記住這一點,插入的時候一個一個比較就好,從根節點開始,大就向右,小就向左,直到最後的NULL,在NULL處插入節點就好。
void insert(const elemType &x){
insert(x, root);
}
void insert(const elemType &x, node *&t){
if(t == NULL){
t = new node(x, NULL, NULL);
}
else{
if(x < t -> data){
insert(x, t -> left);
}
else{
if(x == t -> data){
cout << "The node has existed" << endl;
return;
}
else{
insert(x, t -> right);
}
}
}
}
remove操作:
remove操作是二叉查找樹中最難的,但你也不要害怕,畢竟你都已經看到這兒了對吧,不看完怎麼行。
想想remove其實就分三種情況:
- remove的節點是葉節點,那麼二話不說直接刪了就好
- remove的節點有一個左子樹或者有一個右子樹,那麼直接把這個節點刪去,它的子樹補上來就好
- remove的節點有左子樹右子樹都有,這就比較麻煩了,是不是已經蒙圈了,不知道怎麼補了,不要慌,且待我慢慢給你講解:刪除了這個節點,這個位置又不能空着,所以我們按正常人的思維就找了個替身來填補這個位置,那麼怎樣選擇這個替身才能保證樹仍保持有序呢?靜下心來想想,要想保持有序,那必須補上來左子樹的最大值或是右子樹的最小值啊,其實就是中序遍歷的該值的相鄰兩個值是吧。
找左子樹的最大值,其實就是從左子樹的根節點開始,一直往右找,直到末端,那末端的值必然是左子樹的最大值了,同理,找右子樹的最小值,其實就是從右子樹的根節點開始,一直往左找,直到末端,那末端的值必然是右子樹的最小值了。(代碼以找右子樹的最小值爲例寫的)
找到了替身,使要刪節點的值改爲替身的值,然後刪掉替身就好(有沒有一種恩將仇報的趕腳),這裏的刪掉替身的操作一定是滿足前兩種操作的。
void remove(const elemType &x){
remove(x, root);
}
void remove(const elemType &x, node *&t){//注意這裏是指針的引用,不是複製構造,因爲要滿
//足補上來子樹與上面的節點能夠接上
if(t == NULL){
return;
}
if(x < t -> data){
remove(x, t -> left);
}
else{
if(x > t -> data){
remove(x, t -> right);
}
else{ //==
if(t -> left != NULL && t -> right != NULL){
node *tmp = t -> right;
while(tmp -> left != NULL){
tmp = tmp -> left;
}
t -> data = tmp -> data;
remove(t -> data, t -> right);
}
else{
node *old = t;
if(t -> left == NULL && t -> right == NULL){
delete old;
}
else{
if(t -> left!= NULL){
t = t -> left;
}
else{
t = t -> right;
}
delete old;
}
}
}
}
}
完整代碼:
#include <iostream>
using namespace std;
template<class elemType>
class binarySearchTree
{
private:
struct node{
elemType data;
node *left;
node *right;
node(const elemType &d, node *ln = NULL, node *rn = NULL):data(d), left(ln), right(rn){}
};
node *root;
public:
binarySearchTree(){
root = NULL;
}
~binarySearchTree(){
clear(root);
}
void clear(node *t){
if(t == NULL){
return;
}
clear(t -> left);
clear(t -> right);
delete t;
}
elemType *find(const elemType &x){
return find(x, root);
}
elemType *find(const elemType &x, node *t){
if(t == NULL){
cout << "Can not find" << endl;
return 0;
}
if(t -> data == x){
cout << "haha" << endl;
return &(t -> data);
}
if(x < t -> data){
return find(x, t -> left);
}
else{
return find(x, t -> right);
}
}
void insert(const elemType &x){
insert(x, root);
}
void insert(const elemType &x, node *&t){
if(t == NULL){
t = new node(x, NULL, NULL);
}
else{
if(x < t -> data){
insert(x, t -> left);
}
else{
if(x == t -> data){
cout << "The node has existed" << endl;
return;
}
else{
insert(x, t -> right);
}
}
}
}
void remove(const elemType &x){
remove(x, root);
}
void remove(const elemType &x, node *&t){
if(t == NULL){
return;
}
if(x < t -> data){
remove(x, t -> left);
}
else{
if(x > t -> data){
remove(x, t -> right);
}
else{ //==
if(t -> left != NULL && t -> right != NULL){
node *tmp = t -> right;
while(tmp -> left != NULL){
tmp = tmp -> left;
}
t -> data = tmp -> data;
remove(t -> data, t -> right);
}
else{
node *old = t;
if(t -> left == NULL && t -> right == NULL){
delete old;
}
else{
if(t -> left!= NULL){
t = t -> left;
}
else{
t = t -> right;
}
delete old;
}
}
}
}
}
void midOrder(){
midOrder(root);
}
void midOrder(node *p){
if(p == NULL){
return;
}
midOrder(p -> left);
cout << p -> data << ' ';
midOrder(p -> right);
}
};
總結:
二叉查找樹性能與結構有很大關係,如果構建的好,也就是二叉查找樹接近於一棵完全二叉樹,那麼其所有操作都是O(logn)的,但如果數據很變態,剛好形成了一條鏈,那就是線性O(n)了。
不過總歸是有辦法解決的,那就是傳說中的AVL樹和紅黑樹,之後會慢慢講到。