AVL樹的原理及程式碼實現
前言:
如果你還沒有學習過二叉查詢樹,那麼請你先去看看二叉查詢樹,因為AVL樹便是從二叉查詢樹進化而來的,不看的話你無法理解AVL樹。
如果你已經學習了二叉查詢樹,你會覺得二叉查詢樹效能在各方面都很好,就只有一丟丟的小毛病,那就是當資料非常坑時,二叉查詢樹退化成了一條單鏈表,這樣各種操作的時間複雜度都變為O(n)了,怎麼辦呢,今天所要學習的AVL樹便以其驚豔四座的高階技巧解決了這一問題,使其在任何情況下的各種操作複雜度都為O(logn)。
AVL樹:
AVL樹是根據它的發明者G.M. Adelson-Velsky和E.M. Landis命名的。 它是最先發明的自平衡二叉查詢樹,也被稱為高度平衡樹。
在介紹AVL樹時,首先要介紹一個概念——平衡度。
平衡度 = 左子樹的高度 - 右子樹的高度
當樹上所有節點平衡度為-1,0,1時,我們認為這棵樹是平衡的,當有節點的平衡度絕對值 > 1時,我們認為這棵樹是不平衡的,我們就要對這個節點進行調整。
基本實現
儲存實現:
AVL樹與二叉查詢樹一樣使用二叉連結串列實現,這樣能夠很好的理解,每個節點有一個元素儲存值,兩個指標分別指向它的左子樹和右子樹,還有一個元素來儲存該節點的高度。
template <class elemType> class AvlTree{ private: struct node{ elemType data; node *left; node *right; int height; node(const elemType &x, node *ln, node *rn, int h = 1):data(x), left(ln), right(rn), height(h){} }; node *root; }
find操作:
AVL樹的操作與二叉查詢樹的find操作原理一模一樣,這裡就不詳細講了,想看的可以去文首的二叉查詢樹的連結裡看。
此處程式碼使用了非遞迴實現:
elemType *find(const elemType &x){ node *t = root; while(t != NULL && t -> data != x){ if(t -> data > x){ t = t -> right; } else{ t = t -> left; } } if(t == NULL){ return NULL; } else{ return &(t -> data); } }
midOrder操作:
也與二叉查詢樹完全相同,中序遍歷輸出整個樹的值,結果必然是一個從小到大序列。
void midOrder(){
midOrder(root);
}
void midOrder(node *p){
if(p == NULL){
return;
}
midOrder(p -> left);
cout << p -> data << ' ';
midOrder(p -> right);
}
insert操作:
insert操作就與二叉查詢樹有些不同了,它不但要找到合適位置插入元素,還要判斷插入後是不是破壞了樹的平衡性,如果破壞了要對樹進行調整。
先列舉幾種失去平衡的情況:
LL: 在節點的左子樹的左子樹上插入節點,使節點平衡度變為2,失去平衡
LR:在節點的左子樹的右子樹上插入節點,使節點平衡度變為2,失去平衡
RR:在節點的右子樹的右子樹上插入節點,使節點平衡度變為-2,失去平衡
RL:在節點的右子樹的左子樹上插入節點,使節點平衡度變為-2,失去平衡
下面來說解決辦法:
LL單旋轉:
如圖,k2失去平衡,k2的左子樹根節點k1頂替k2的位置,且k2的左指標指向其k1的右節點,k1的右指標指向k2,這樣整個樹恢復了平衡。
void LL(node *t){
node *t1 = t -> left;
t -> left = t1 -> right;
t1 -> right = t;
t -> height = max(high(t -> left), high(t -> right)) + 1;
t1 -> height = max(high(t1 -> left), high(t1 -> right)) + 1;
t = t1;
}
RR單旋轉:
如圖,k1失去平衡,k1的左子樹根節點k2頂替k1的位置,且k1的右指標指向其k2的左節點,k2的左指標指向k1,這樣整個樹恢復了平衡。
void RR(node *t){
node *t1 = t -> right;
t -> right = t1 -> left;
t1 -> left = t;
t -> height = max(high(t -> left), high(t -> right)) + 1;
t1 -> height = max(high(t1 -> left), high(t1 -> right)) + 1;
t = t1;
}
LR雙旋轉:
如圖,k3失去了平衡,如果只對k3使用LL旋轉,那樣k2作為k3的子樹,k1節點則會失去平衡,這時就需要兩次旋轉來實現。先對k1進行RR旋轉,再對k3進行LL旋轉,這樣就恢復了平衡,是不是被如此酷炫的操作亮瞎了雙眼。來看看程式碼實現:
void LR(node *t){
RR(t -> left);
LL(t);
}
RL雙旋轉:
與LR對稱著來看就好。
void RL(node *t){
LL(t -> right);
RR(t);
}
學會了這些操作之後,insert操作就很好理解了,只要在插入後判斷一下是否平衡,若不平衡,對症下藥調整一下就好。注意要注意每次插入後要從插入點到根節點一個一個更新height值,也就是程式碼的最後一行。
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);
if(high(t -> left) - high(t -> right) == 2){
if(x < t -> left -> data){
LL(t);
}
else{
LR(t);
}
}
}
else{
if(x == t -> data){
cout << "The node has existed" << endl;
return;
}
else{
insert(x, t -> right);
if(high(t -> right) - high(t -> left) == 2){
if(x > t -> right -> data){
RR(t);
}
else{
RL(t);
}
}
}
}
}
t -> height = max(high(t -> left), high(t -> right)) + 1; //!!!
}
remove操作:
同樣的道理,remove操作也在二叉查詢樹的基礎上增加了判平衡操作,這裡我們使用到了反向思維
這是RL操作的圖:
舉個栗子,假入說A下面原來掛著一個x,現在我們把它刪了,是不是就跟上述情況一樣了呢。
其他幾種情況自己對照著上面的四種情況這樣理解一下,相信你會恍然大悟的。
void remove(const elemType &x, node *&t){
if(t == NULL){
return;
}
if(x < t -> data){
remove(x, t -> left);
if(high(t -> right) - high(t -> left) == 2 ){
if(t -> right -> left != NULL && high(t -> right -> left) > high(t -> right -> right)){
RL(t);
}
else{
RR(t);
}
}
}
else{
if(x > t -> data){
remove(x, t -> right);
if(high(t -> left) - high(t -> right) == 2 ){
if(t -> left -> right != NULL && high(t -> left -> right) > high(t -> left -> left)){
LR(t);
}
else{
LL(t);
}
}
}
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);
if(high(t -> left) - high(t -> right) == 2 ){
if(t -> left -> right != NULL && high(t -> left -> right) > high(t -> left -> left)){
LR(t);
}
else{
LL(t);
}
}
}
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;
}
}
}
}
t -> height = max(high(t -> left), high(t -> right)) + 1;
}
完整程式碼:
#include <iostream>
using namespace std;
template <class elemType>
class AvlTree{
private:
struct node{
elemType data;
node *left;
node *right;
int height;
node(const elemType &x, node *ln, node *rn, int h = 1):data(x), left(ln), right(rn), height(h){}
};
node *root;
public:
AvlTree(){
root = NULL;
}
~AvlTree(){
clear(root);
}
void clear(node *t){
if(t == NULL){
return;
}
clear(t -> left);
clear(t -> right);
delete t;
}
elemType *find(const elemType &x){
node *t = root;
while(t != NULL && t -> data != x){
if(t -> data > x){
t = t -> right;
}
else{
t = t -> left;
}
}
if(t == NULL){
return NULL;
}
else{
return &(t -> data);
}
}
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);
if(high(t -> left) - high(t -> right) == 2){
if(x < t -> left -> data){
LL(t);
}
else{
LR(t);
}
}
}
else{
if(x == t -> data){
cout << "The node has existed" << endl;
return;
}
else{
insert(x, t -> right);
if(high(t -> right) - high(t -> left) == 2){
if(x > t -> right -> data){
RR(t);
}
else{
RL(t);
}
}
}
}
}
t -> height = max(high(t -> left), high(t -> right)) + 1;
}
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);
if(high(t -> right) - high(t -> left) == 2 ){
if(t -> right -> left != NULL && high(t -> right -> left) > high(t -> right -> right)){
RL(t);
}
else{
RR(t);
}
}
}
else{
if(x > t -> data){
remove(x, t -> right);
if(high(t -> left) - high(t -> right) == 2 ){
if(t -> left -> right != NULL && high(t -> left -> right) > high(t -> left -> left)){
LR(t);
}
else{
LL(t);
}
}
}
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);
if(high(t -> left) - high(t -> right) == 2 ){
if(t -> left -> right != NULL && high(t -> left -> right) > high(t -> left -> left)){
LR(t);
}
else{
LL(t);
}
}
}
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;
}
}
}
}
t -> height = max(high(t -> left), high(t -> right)) + 1;
}
int high(node *t){
if(t == NULL){
return 0;
}
else{
return t -> height;
}
}
void LL(node *t){
node *t1 = t -> left;
t -> left = t1 -> right;
t1 -> right = t;
t -> height = max(high(t -> left), high(t -> right)) + 1;
t1 -> height = max(high(t1 -> left), high(t1 -> right)) + 1;
t = t1;
}
void LR(node *t){
RR(t -> left);
LL(t);
}
void RR(node *t){
node *t1 = t -> right;
t -> right = t1 -> left;
t1 -> left = t;
t -> height = max(high(t -> left), high(t -> right)) + 1;
t1 -> height = max(high(t1 -> left), high(t1 -> right)) + 1;
t = t1;
}
void RL(node *t){
LL(t -> right);
RR(t);
}
int max(int a, int b){
if(a > b){
return a;
}
else{
return b;
}
}
void midOrder(){
midOrder(root);
}
void midOrder(node *p){
if(p == NULL){
return;
}
midOrder(p -> left);
cout << p -> data << ' ';
midOrder(p -> right);
}
};
總結
AVL樹很好的解決了平衡二叉樹在特殊情況下會退化成單鏈表的問題,這樣AVL樹就一直會保持在矮胖的狀態,而不會成為一顆瘦高的樹。AVL樹的查詢、插入和刪除在平均和最壞情況下都是O(logn)。只要學好AVL樹的旋轉操作,就能學好AVL樹。