文章目錄
1、平衡二叉樹
1.1、什麼是二叉搜索樹
1.2、二叉搜索樹的缺點
二叉樹查找算法查找時間依賴於樹的拓撲結構。查找效率取決於樹的高度,因此保持樹的高度最小,即可保證樹的查找效率。最佳情況是 O(log2n),而最壞情況是 O(n)。如果建立的二叉搜索樹不平衡了。極端不平衡的情況就是類似一個單鏈表,這個時候,查找就需要O(n)時間複雜度。
1.3、平衡二叉樹的提出
假設我們能夠對二叉樹進行調正,使得二叉查找樹總保持平衡或者不會出現太不平衡的情況。那不就能解決二叉查找樹極端情況的出現了從而保證了二叉查找算法的效率了嗎。
平衡二叉樹的定義
簡稱平衡二叉樹。由前蘇聯的數學家 Adelse-Velskil 和 Landis
在 1962 年提出的高度平衡的二叉樹,根據科學家的英文名也稱爲 AVL 樹。它具有如下幾個性質:
- 可以是空樹。
- 假如不是空樹,任何一個結點的左子樹與右子樹都是平衡二叉樹,並且高度之差的絕對值不超過 1。
平衡因子
定義:某結點的左子樹與右子樹的高度(深度)差
即爲該結點的平衡因子(BF,Balance Factor
),平衡二叉樹中不存在平衡因子大於 1 的節點。在一棵平衡二叉樹中,節點的平衡因子只能取 0 、1 或者 -1 ,分別對應着左右子樹等高,左子樹比較高,右子樹比較高。
ALV結構定義
typedef struct AVLNode *Tree;
typedef int ElementType;
struct AVLNode{
int depth; //深度,這裏計算每個結點的深度,通過深度的比較可得出是否平衡
Tree parent; //該結點的父節點
ElementType val; //結點值
Tree lchild;
Tree rchild;
AVLNode(int val=0) {
parent = NULL;
depth = 0;
lchild = rchild = NULL;
this->val=val;
}
};
1.4、如何構建平衡二叉樹(ALV樹)
最小失衡子樹:在新插入的結點
向上查找,以第一個平衡因子的絕對值超過 1 的結點爲根的子樹稱爲最小不平衡子樹。也就是說,一棵失衡的樹,是有可能有多棵子樹同時失衡的
。而這個時候,我們只要調整最小的不平衡子樹,就能夠將不平衡的樹調整爲平衡的樹。
平衡二叉樹的失衡調整主要是通過旋轉最小失衡子樹來實現的。根據旋轉的方向有兩種處理方式,左旋
與 右旋
。
旋轉的目的就是減少高度,通過降低整棵樹的高度來平衡。哪邊的樹高,就把那邊的樹向上旋轉。
1.5 失衡情況及其處理(4種)
ALV樹的失衡主要是在插入和刪除結點的時候發生的。如:
插入結點:
刪除結點:
失衡的情況主要有:
A原本是一棵平衡二叉樹,現在執行插入操作,可能的情況如表格所示:
插入方式 | 描述 | 旋轉方式 |
---|---|---|
LL | 在 A 的左子樹根節點的左子樹上插入節點而破壞平衡 | 右旋轉 |
RR | 在 A 的右子樹根節點的右子樹上插入節點而破壞平衡 | 左旋轉 |
LR | 在A的左子樹根節點的右子樹上插入節點而破壞平衡 | 先左旋後右旋 |
RL | 在 A 的右子樹根節點的左子樹上插入節點而破壞平衡 | 先右旋後左旋 |
1.5.1 LL——>(右旋)
只需要執行一次右旋:(1)將失衡結點A變爲其左孩子B的右孩子(2)將其原本的左孩子B的右孩子E變爲A的左孩子
1.5.2 RR——>(左旋)
只需要執行一次左旋:(1)將失衡結點A變爲其右孩子C的左孩子(2)將其原本的右孩子C的左孩子D變爲A的右孩子
1.5.3 LR——>(先左旋再右旋)
若 A 的左孩子節點 B 的右子樹 E 插入節點 F ,導致結點 A 失衡,如圖:
那我們該怎麼做呢?先嚐試以下右旋:
經過右旋調整發現,調整後樹仍然失
衡,說明這種情況單純的進行右旋操作不能使樹重新平衡。那麼這種插入方式需要執行兩步操作
- (1)先對失衡節點 A 的左孩子 B 進行左旋操作,即上述 RR 情形操作。
- (2)對失衡節點 A 做右旋操作,即上述 LL 情形操作。
也就是說,經過這兩步操作,使得原來根結點的左孩子的右孩子 E 節點成爲了新的根節點。
1.5.4 RL——>(先右旋再左旋)
右孩子插入左節點的過程與左孩子插入右節點過程類似,也是需要執行兩步操作,使得旋轉之後爲 原來根結點的右孩子的左孩子作爲新的根節點 。(A->D)
- (1)先對失衡節點 A 的右孩子 C 進行右旋操作,即上述 LL 情形操作。
- (2)對失衡節點 A 做左旋操作,即上述 RR情形操作。
1.5.5 總結代碼實現
#include<iostream>
#include<cstdio>
#include<cstdlib>
typedef int ElenmentType;
//平衡二叉樹的結構
typedef struct AVLNode{
int depth;//深度
struct AVLNode *left;
struct AVLNode *right;
struct AVLNode *parent;
ElenmentType value;
//構造器
AVLNode(ElenmentType value=0){
parent = NULL;
depth = 0;
left = right = NULL;
this->value=value;
}
}AVLtree,Tree;
//LL型調整函數
//返回根結點
Tree* LL_rotate(Tree *root){//LL執行右旋
//root是原來的平衡二叉樹的根結點
Tree *temp;//臨時變量
//獲取根結點的左孩子
temp = root->left;
//根結點的左孩子變更爲其原來左孩子的右孩子
root->left = temp->right;
//原來的根結點的左孩子變爲了根結點
temp->right = root;
return temp;
}
//RR型調整函數
//返回根結點
Tree* RR_rotate(Tree * root){//RR執行左旋
Tree* temp;
temp = root->right;//獲取根結點的右孩子
root->right = temp->left;//根結點的右孩子變爲其原來右孩子的左孩子
temp->left = root;//原來的根結點的右孩子變爲了新的根結點
return temp;
}
//1、LR型,先左旋轉,再右旋轉
//返回:新父節點
Tree* LR_rotate(Tree* root){
Tree* temp;
temp = root->left;
root->left =RR_rotate(temp);
return LL_rotate(root);
}
//2RL型,先右旋轉,再左旋轉
//返回:新父節點
Tree* RL_rotate(Tree* root){
Tree* temp;
temp = root->right;
root->right=LL_rotate(temp);
return RR_rotate(root);
}
//樹高
int height(const Tree* root)//求樹高,遞歸
{
if (root == NULL)
return 0;
return std::max(height(root->left) ,
height(root->right)) + 1;
}
//平衡因子
int diff(const Tree* root)//求平衡因子,即當前節點左右子樹的差
{
return height(root->left) - height(root->right);
}
//平衡操作
Tree* Balance(Tree* root)
{
// printf("平衡函數");
int balanceFactor = diff(root);//diff用來計算平衡因子(左右子樹高度差)
if (balanceFactor > 1)//左子樹高於右子樹
{
if (diff(root->left) > 0)//LL的情況
root=LL_rotate(root);
else//LR的情況
root=LR_rotate(root);
}
else if (balanceFactor < -1)//右子樹高於左子樹
{
if (diff(root->right) > 0)//RL的情況
root=RL_rotate(root);
else//RR的情況
root=RR_rotate(root);
}
return root;
}
//插入結點
Tree* Insert(Tree* root,ElenmentType k )
{
if (NULL == root)
{
root = new AVLNode(k);//如果根結點爲null,則直接將值爲根結點
if(root==NULL)
printf("創建失敗");
return root;
}//遞歸返回條件
else if (k < root->value)
{
root->left = Insert(root->left, k);//遞歸左子樹
//balance operation
root = Balance(root);//平衡操作包含了四種旋轉
}
else if (k>root->value)
{
root->right = Insert(root->right, k);//遞歸右子樹
//balance operation
root = Balance(root);//平衡操作包含了四種旋轉
}
return root;
}
//中序遍歷,獲取的數列是有序的
void displayTree(Tree* node){
if(node == NULL) return;
if(node->left != NULL){
displayTree(node->left);
}
printf("%d ",node->value);
if(node->right != NULL){
displayTree(node->right);
}
}
//查找value,成功則返回該結點
Tree* binaryTreeSearch(Tree *node,int value){
if(node->value==value)
return node;
else if(node->value>value){
if(node->left!=NULL)
return binaryTreeSearch(node->left,value);
else return NULL;
}else{
if(node->right!=NULL)
return binaryTreeSearch(node->right,value);
else
return NULL;
}
}
int main(){
int a[10]={12,13,55,66,44,77,99,33,46,79};
Tree *root=NULL;
printf("第一次構建的平衡二叉樹中序遍歷:");
for(int i=0;i<10;i++){
root = Insert(root,a[i]);
}
displayTree(root);
printf("\n");
//查找33
int value = 33;
Tree* obj;
if((obj=binaryTreeSearch(root,value))==NULL){
printf("%d值不存在",value);
}
else printf("%d值存在,地址是%p",value,obj);
printf("\n");
root = Insert(root,5);
printf("插入了結點值爲5的結點以後,中序遍歷:") ;
displayTree(root);
return 0;
}
運行結果
1.5.6 小結
在所有的不平衡情況中,都是按照先 尋找最小不平衡樹,然後 尋找所屬的不平衡類別,再 根據 4 種類別進行固定化程序的操作。
LL , LR ,RR ,RL其實已經爲我們提供了最後哪個結點作爲新的根指明瞭方向。如 LR 型最後的根結點爲原來的根的左孩子的右孩子,RL 型最後的根結點爲原來的根的右孩子的左孩子。只要記住這四種情況,可以很快地推導出所有的情況。
維護平衡二叉樹,最麻煩的地方在於平衡因子的維護。
1.5.7 刪除結點後該如何調整平衡二叉樹?
AVL 樹和二叉查找樹的刪除操作情況一致,都分爲四種情況:
+(1)刪除葉子節點
+(2)刪除的節點只有左子樹
+(3)刪除的節點只有右子樹
+(4)刪除的節點既有左子樹又有右子樹
只不過
AVL 樹在刪除節點後需要重新檢查平衡性並修正
,同時,刪除操作與插入操作後的平衡修正區別在於,插入操作後只需要對插入棧中的彈出的第一個非平衡節點進行修正,而刪除操作需要修正棧中的所有非平衡節點。
刪除操作的大致步驟如下:
- 1、 以前三種情況爲基礎嘗試刪除節點,並將訪問節點入棧。
如果嘗試刪除成功,則依次檢查棧頂節點的平衡狀態,遇到非平衡節點,即進行旋轉平衡,直到棧空。
如果嘗試刪除失敗,證明是第四種情況。這時先找到被刪除節點的右子樹最小節點並刪除它,將訪問節點繼續入棧。 - 再依次檢查棧頂節點的平衡狀態和修正直到棧空。
對於刪除操作造成的非平衡狀態的修正,可以這樣理解:對左或者右子樹的刪除操作相當於對右或者左子樹的插入操作,然後再對應上插入的四種情況選擇相應的旋轉就好了。
代碼實現:
//刪除結點
Tree* Delete(Tree *root, const ElenmentType k)
{
if (NULL == root)
return root;
if (!binaryTreeSearch(root,k))//查找刪除元素是否存在
{
std::cerr << "Delete error , key not find" << std::endl;
return root;
}
if (k == root->value)//根節點
{
if (root->left!=NULL&&root->right!=NULL)//左右子樹都非空
{
if (diff(root) > 0)//左子樹更高,在左邊刪除
{
root->value = tree(root->left,0);//以左子樹的最大值替換當前值
root->left = Delete(root->left, root->value);//刪除左子樹中已經替換上去的節點
}
else//右子樹更高,在右邊刪除
{
root->value = tree(root->right,1);
root->right = Delete(root->right, root->value);
}
}
else//有一個孩子、葉子節點的情況合併
{
Tree * tmp = root;
root = (root->left) ? (root->left) :( root->right);
delete tmp;
tmp = NULL;
}
}
else if (k < root->value)//往左邊刪除
{
root->left = Delete(root->left, k);//左子樹中遞歸刪除
//判斷平衡的條件與在插入時情況類似
if (diff(root) < -1)//不滿足平衡條件,刪除左邊的後,右子樹變高
{
if (diff(root->right) > 0)
{
root = RL_rotate(root);
}
else
{
root = RR_rotate(root);
}
}
}//end else if
else
{
root->right = Delete(root->right, k);
if (diff(root) > 1)//不滿足平衡條件
{
if (diff(root->left) < 0)
{
root = LR_rotate(root);
}
else
{
root = LL_rotate(root);
}
}
}
return root;
}
運行結果:
1.6 評價
平衡二叉樹在查找的時候就有二叉查找樹的優勢,同時也保持了平衡。
但是平衡二叉樹的劣勢在於:
- 刪除:對於平衡二叉樹來說,在最壞情況下,需要維護從被刪節點到根節點這條路徑上所有節點的平衡性,旋轉的量級是OlogN。但是紅黑樹就不一樣了,最多隻需3次旋轉就會重新平衡,旋轉的量級是O(1)。
- 保持平衡:平衡二叉樹高度平衡,這也就意味着在大量插入和刪除節點的場景下,平衡二叉樹爲了保持平衡需要調整的頻率會更高。
所以在大量查找的情況下,平衡二叉樹的效率更高
,也是首要選擇。在大量增刪的情況下,紅黑樹是首選。
參考博客:
https://blog.csdn.net/zhangxiao93/article/details/51459743
https://zhuanlan.zhihu.com/p/56066942