本節重點
- 理解AVL樹的概念
- 掌握AVL樹正確的插入方法
- 利用_parent指針正確更新平衡因子
- 掌握并理解四種旋轉方式:左單旋,右單旋,左右雙旋,右左雙旋
一、AVL樹的概念
AVL樹得名于它的發明者G. M. Adelson-Velsky和E. M. Landis,他們在1962年的論文《An algorithm for the organization of information》中發表了它。
AVL樹最先發明自平衡二叉搜索樹,AVL是一顆空樹,或者具備下列性質的二叉搜索樹:它的左右子樹都是AVL樹,且左右子樹的高度差的絕對值不超過1。AVL樹是一顆高度平衡搜索二叉樹,通過控制高度差去控制平衡。
AVL樹的實現引入一個平衡因子的概念(balance factor)的概念,每個節點都有一個平衡因子,任何節點的平衡因子等于右子樹高度減去左子樹高度,也就是說任何節點的平衡因子等于0/1/-1,AVL樹并不是必須要平衡因子,但是有了平衡因子可以更方便我們去進行觀察和控制樹是否平衡,就像一個風向標一樣。
AVL樹整體的節點數量和分布和完全二叉樹類似,但是AVL樹高度可以控制在logN,所以增刪查改的效率也可以控制在O(logN),相比二叉搜索樹有了本質的提升。
如圖,每個節點上方的小數字表示該節點的平衡因子,平衡因子只能為-1/1/0當為2/-2時我們要通過旋轉將左右子樹重新達到平衡狀態。?
二、AVL樹的實現
2.1 AVL樹的結構
AVL樹我們分成兩部分來實現,一部分是單個節點的定義,一部分是AVL樹。并且用兩個類進行封裝:
template<class K>
struct AVLTNode//AVL樹節點的定義
{K _key;struct AVLTNode<K>* _left;struct AVLTNode<K>* _right;struct AVLTNode<K>* _parent;//引入parent方便我們快速向上更新平衡因子int _bf;//平衡因子(balance factor)//構造函數:AVLTNode(K key):_key(key), _left(nullptr), _right(nullptr), _parent(nullptr),_bf(0){}
};//AVL樹的定義
template<class K>
class AVLTree
{typedef struct AVLTNode<K> Node;
public:
private:Node* root = nullptr;
};
2.2 AVL樹的插入
AVL樹插入的步驟:
- 插入一個值按照二叉搜索樹的規則插入
- 新增節點后,只會影響祖先節點的高度,也就是可能會影響部分祖先節點的平衡因子,所以更新從新增節點->根節點路徑上的平衡因子,實際最壞情況下更新到根,有些情況更新到中間就停止了。
- 更新平衡因子過程中沒有出現問題,則插入結束
- 更新平衡因子的過程中出現不平衡,對不平衡子樹旋轉,旋轉后降低了子樹的高度,不會再影響上一層,所以插入結束
2.2.1 先按照搜索二叉樹規則插入
在插入之前,我們需要判斷該AVL樹是否為空,若為空直接在_root 新增節點并返回,若不為空說明AVL樹中已經存在節點,這時我們需要從根節點開始依次按照搜索樹的規則尋找插入位置,找到之后創建新節點并鏈入到AVL樹中。
代碼示例:
bool Insert(const K& key){if (_root == nullptr){//AVL是一顆空樹_root=new Node(key)return 1;}else{Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{assert(false);}}//鏈接新節點:cur = new Node(key);if (cur->_key > parent->_key){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent;}
2.2.2 判斷并更新平衡因子
更新規則:
平衡因子=右子樹高度-左子樹高度
插入節點會增加子樹高度,影響當前節點平衡因子因為一次只能插入一個節點所以平衡因子要么++要么--:
插入在右子樹平衡因子++;插入在左子樹平衡因子--。
平衡因子的三種情況:(0,-1/1,-2/2)
1、更新后parent節點平衡因子為0
說明更新之前parent節點平衡因子為1或-1也就是左右子樹一邊高一邊低,節點插入在低的一邊,插入后左右平衡不會影響上一級節點的平衡因子。
2、更新后parent節點平衡因子為1/-1
說明更新之前parent節點平衡因子為0,插入后左右子樹一邊高一邊低會影響上一級節點的平衡因子,所以要繼續向上更新。
3、更新后parent節點平衡因子為2/-2
說明更新之前parent節點平衡因子為1/-1也就是左右子樹一邊高一邊低,節點插入在高的一邊
代碼示例:?
while (parent)
{if (parent->_right == cur){parent->_bf++;}else{parent->_bf--;}if (parent->_bf == 0){//說明新增節點在低的一邊,插入后左右子樹平衡//不會影響祖先節點的 _bf直接breakbreak;}else if(parent->_bf==1||parent->_bf==-1){//新增節點之后為1或-1說明之前為0(左右子樹平衡)//此時需要更新依次更新祖先節點的 _bf直到更新到根節點或某一祖先節點_bf==0cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){if (parent->_bf == 2 && cur->_bf == 1){//單純右邊高,左旋RotateL(parent);}else if (parent->_bf == -2 && cur->_bf == -1){//單純左邊高,右旋RotateR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){//先左后右,右左雙旋RotateRL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){//先右后左,左右雙旋RotateLR(parent);}else{assert(false);}break;}else{assert(false);}
}
2.2.3 (不平衡)旋轉子樹
旋轉的原則:
- 保持搜索樹的規則
- 讓旋轉的樹從不滿足變為平衡,其次降低樹的高度
首先我們需要明白的是旋轉操作分為兩部分,一是調整節點之間的鏈接關系,二是更新平衡因子? ? ?_bf
左單旋(RotateL)
當parent的平衡因子為2,且cur的平衡因子為1時AVL樹會呈現右子樹一邊高的形式,這時我們需要進行左旋操作,需要注意的是滿足 parent->_bf==2 && cur->_bf==1 條件的AVL樹的形式可能有多種:
為了便于理解,我們可以選擇一種簡單的情況進行分析并寫出相應代碼:
代碼示例:
void RotateL(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;Node* pparent = parent->_parent;if (SubRL){SubRL->_parent = parent;}parent->_right = SubRL;SubR->_left = parent;parent->_parent = SubR;if (parent == _root){_root = SubR;SubR->_parent = nullptr;}else{if (pparent->_right == parent){pparent->_right = SubR;}else{pparent->_left = SubR;}SubR->_parent = pparent;}//節點鏈接關系調整完成后更新平衡因子:SubR->_bf = 0;parent->_bf = 0;}
右單旋(RotateR)
類似的是,滿足 parent->_bf==-2 && cur->_bf==-1 條件的AVL樹的形式也可能有多種
我們也選擇其中一種簡單的情況進行分析和編寫代碼:
void RotateR(Node* parent){Node* SubL = parent->_left;Node* SubLR = SubL->_right;Node* pparent = parent->_parent;if (SubLR){SubLR->_parent = parent;}parent->_left = SubLR;SubL->_right = parent;parent->_parent = SubL;if (parent == _root){_root = SubL;SubL->_parent = nullptr;}else{if (pparent->_right == parent){pparent->_right = SubL;}else{pparent->_left = SubL;}SubR->_parent = pparent;}//節點鏈接關系調整完成后更新平衡因子:SubL->_bf = 0;parent->_bf = 0;}
左右雙旋(RotateLR)
與單旋不同的是,雙旋對應的AVL樹的結構不再是單純一邊高,我們由條件判斷很容易就可以看出來(parent->_bf == -2 && cur->_bf == 1 或 parent->_bf == 2 && cur->_bf == -1),此時我們發現AVL樹節點類似“折線形”排列,這時單純的單旋無法使二叉樹再次平衡,我們需要進行兩次單旋來解決。
類似的是滿足雙旋觸發條件時,AVL樹的結構要是拓展開來也有非常多種情況,我們可以選擇其中一種較為簡單的情況來分析并編寫相應代碼:
需要注意的是左右雙旋的時候對SubLR的_bf也要進行考慮,目的是確定SubLR是否存在單個的子樹,因為最終SubLR的子樹會鏈入SubL或者parent影響兩個節點的平衡因子。
void RotateLR(Node* parent){Node* SubL = parent->_left;Node* SubLR = SubL->_right;int bf = SubLR->_bf;RotateL(parent->_left);RotateR(parent);if (bf == 1){SubLR->_bf = 0;parent->_bf = 0;SubL->_bf = -1;}else if (bf == 0){parent->_bf = 0;SubLR->_bf = 0;SubL->_bf = 0;}else{SubLR->_bf = 0;parent->_bf = 1;SubL->_bf = 0;}}
右左雙旋(RotateRL)
?
void RotateRL(Node* parent){Node* SubR = parent->_right;Node* SubRL = SubR->_left;int bf = SubRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 1){SubRL->_bf = 0;parent->_bf = -1;SubR->_bf = 0;}else if (bf == 0){parent->_bf = 0;SubLR->_bf = 0;SubL->_bf = 0;}else{SubRL->_bf = 0;parent->_bf = 0;SubR->_bf = 1;}}