AVL樹簡介算法
AVL樹是一種高度平衡的二叉樹,在定義樹的每一個結點的同時,給樹的每個結點增長成員 平衡因子bf ,定義平衡因子爲右子樹的高度減去左子樹的高度。AVL樹要求全部節點左右子樹的高度差不超過2,即bf的絕對值小於2。ide
當咱們插入新的結點以後,平衡樹的平衡狀態將會被破壞,所以咱們須要採用相應的調整算法使得樹從新迴歸平衡。
函數
預備知識spa
前文說當插入新的結點時,樹的結構可能會發生破壞,所以咱們設定了一套調整算法。調整可分爲兩類:一類是結構調整,即改變樹中結點的鏈接關係,另外一類是平衡因子的調整,使平衡因子從新知足AVL樹的要求。調整過程包含四個基本的操做,左旋轉,右旋轉,右左雙旋,左右雙旋。 3d
平衡樹的旋轉,目的只有一個,下降樹的高度,高度下降以後,就大大簡化了在樹中查找結點時間複雜度。指針
左旋: orm
十、20爲樹的三個結點。當在20的右子樹插入一個結點以後,如圖。當Parent結點的平衡因子爲2,cur結點的平衡因子爲1時進行左旋。blog
將 parent 的 right 指針,指向cur 的left結點;同時cur的left 指針,指向parent 結點。cur 結點繼承了原來parent結點在該樹(子樹)中的根節點的位置,若是原來的parent結點還有父結點,cur須要和上一層的結點保持鏈接關係。(這裏咱們容許cur的左子樹爲NULL)繼承
能夠看到,旋轉以後,原來的parent結點和cur結點的平衡因子都變爲0 。get
//左旋轉代碼實現: void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; parent->_right = subRL; if (subRL != NULL) subRL->_parent = parent; Node* ppNode = parent->_parent; subR->_left = parent; parent->_parent = subR; if (ppNode == NULL) { _root = subR; subR->_parent = NULL; } else { if (parent == ppNode->_left) ppNode->_left = subR; if (parent == ppNode->_right) ppNode->_right = subR; subR->_parent = ppNode; } parent->_bf = 0; subR->_bf = 0; }
右旋和左旋的原理相似,和左旋成鏡像關係。當parent結點的平衡因子變爲 -2,cur結點的平衡因子變爲-1 時,進行右旋。
將 parent 結點的左指針,指向cur結點的右子樹,cur結點的右指針,指向parent結點。同時,cur結點將要繼承在該子樹中parent結點的根節點的位置。即若是parent結點有它本身的父節點,cur將要和parent結點的父節點保持指向關係。(這裏一樣容許cur的右子樹爲NULL)
旋轉以後,也能夠發現,parent 和 cur結點的平衡因子都變爲0。
//右旋轉代碼實現 void RotateR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; parent->_left = subLR; if (subLR != NULL) { subLR->_parent = parent; } Node* ppNode = parent->_parent; subL->_right = parent; parent->_parent = subL; if (ppNode == NULL) { _root = subL; subL->_parent = NULL; } else { if (parent == ppNode->_left) ppNode->_left = subL; else ppNode->_right = subL; subL->_parent = ppNode; } parent->_bf = 0; subL->_bf = 0; }
右左雙旋:
理解了左單旋和右單旋的狀況,雙旋實現起來就簡單了些。
上圖給出了右左雙旋的狀況,能夠看到,當parent 的平衡因子爲2,cur 的平衡因子爲-1時,知足右左雙旋的狀況。
右左雙旋的實現,可分爲三步。
1>以parent->_right 結點爲根進行右旋轉
2>以parent結點爲根進行左旋轉
3>進行調整。
前兩步應該理解起來問題不大,但右左旋轉以後,爲何還要多一步調整呢?緣由就在於個人新增結點是在key=20結點(cur結點的左孩子)的左子樹仍是右子樹插入的,還有可能20就是個人新增結點,即h=0。三種狀況形成的直接後果就是cur的左孩子結點的平衡因子不一樣。這將是咱們區分三種狀況的依據。
這裏有個問題值得注意,爲了提升代碼的複用性,咱們在雙旋的實現中調用了單旋的函數,但在單旋最後,咱們都會將parent 和cur 結點的bf 置0。所以,在單旋以前咱們須要保存cur->_left結點的平衡因子。(如上圖)
//右左旋轉 void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; size_t bf = subRL->_bf; RotateR(parent->_right); RotateL(parent); if (bf == 0) { parent->_bf = 0; subR->_bf = 0; subRL->_bf = 0; } else if (bf == 1) { subR->_bf = 0; parent->_bf = -1; subRL->_bf = 0; } else { parent->_bf = 0; subR->_bf = 1; subRL->_bf = 0; } }
左右雙旋:
左右雙旋和右左雙旋其實也差很少,當知足parent的平衡因子爲-2,且cur 的平衡因子爲1時,進行左右雙旋。
和右左雙旋的概念相似,咱們依舊要先調用單旋函數,以後再進行調整。也須要注意插入節點的位置不一樣帶來的影響,提早對cur的右節點的平衡因子進行保存。這裏一樣給出圖示和代碼,再也不過多贅述。
//左右雙旋 void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; size_t bf = subLR->_bf; RotateL(parent->_left); RotateR(parent); if (bf == 0) { parent->_bf = 0; subL->_bf = 0; subLR->_bf = 0; } else if (bf == 1) { parent->_bf = 0; subL->_bf = -1; subLR->_bf = 0; } else { parent->_bf = 1; subL->_bf = 0; subLR->_bf = 0; } }
插入算法
首先咱們給出結點的定義和相應的構造函數,其中,_key爲關鍵碼,_value爲值。
template <typename K, typename V> struct AVLTreeNode { int _bf; K _key; V _value; AVLTreeNode<K, V>* _left; AVLTreeNode<K, V>* _right; AVLTreeNode<K, V>* _parent; AVLTreeNode(const K& key, const V& value) :_bf(0) , _key(key) , _value(value) , _left(NULL) , _right(NULL) , _parent(NULL) {} };
接下來咱們分析的是插入結點的幾種狀況:
1、樹爲空樹(_root == NULL)
給根節點開闢空間並賦值,直接結束
if (_root == NULL) { _root = new Node(k, v); return true; }
2、樹不爲空樹
要在樹中插入一個結點,大體可分爲幾步。
1> 找到該結點的插入位置
2> 插入結點以後,調整該結點與parent結點的指向關係。
3> 向上調整插入結點祖先結點的平衡因子。
因爲AVL樹是二叉搜索樹,經過循環,比較待插入結點的key值和當前結點的大小,找到待插入結點的位置。同時給該節點開闢空間,肯定和parent節點的指向關係。
//找到待插入結點位置 Node* cur = _root; Node* parent = NULL; while (cur != NULL) { parent = cur; if (k > cur->_key) { cur = cur->_right; } else if (k < cur->_key) { cur = cur->_left; } else { return false; } } //插入節點,創建指向關係 cur = new Node(k, v); if (k < parent->_key) { parent->_left = cur; cur->_parent = parent; } else { parent->_right = cur; cur->_parent = parent; }
插入結點以後,對該AVL樹結點的平衡因子進行調整。因爲插入一個結點,其祖先結點的循環因子均可能發生改變,因此採用循環的方式,向上調整循環因子。
由上圖可知,當插入節點以後,該結點的向上的全部祖先結點的平衡因子並非都在變化,當向上調整直到某一結點的平衡因子變爲 0 以後,將再也不向上調整,由於此時再向上的結點的左右子樹高度差沒有發生變化。
接下來是向上調整平衡因子。
因爲存在要向上調整,這裏定義兩個指針,parent 指針和 cur 指針。當開始循環以後,首先進行調整 parent 指針的平衡因子。調整以後,判斷平衡因子。
平衡因子爲 0 ,則直接跳出循環。
平衡因子爲 1 或 -1 時,繼續向上調整,進行下次循環。
平衡因子爲 2 或 -2 時,就要用到咱們一開始提到的算法--->平衡樹的旋轉
while (parent) { //調整parent的bf if (k < parent->_key) { parent->_bf--; } else { parent->_bf++; } //若是parent的bf爲0,表面插入結點以後,堆parent以上節點的bf無影響 if (parent->_bf == 0) { return true; } else if (abs(parent->_bf) == 1) //爲一、-1時繼續向上調整 { cur = parent; parent = cur->_parent; } else//二、-2 爲二、-2時進行旋轉調整 { if (parent->_bf == 2) { if (cur->_bf == 1) { RotateL(parent); break; } else if (cur->_bf == -1) { RotateRL(parent); break; } } else//parent->_bf == -2 { if (cur->_bf == -1) { RotateR(parent); break; } else if (cur->_bf == 1) { RotateLR(parent); break; } } } }
到這裏,插入算法就已經結束,接下來給出兩個函數,用以對咱們剛剛構建好的AVL樹進行判斷,看是否知足咱們的條件。
bool IsBalance() { int sz = 0; return _IsBalance_better(_root, sz); } bool _IsBalance(Node* root,int& height) { if (root == NULL) return true; int leftheight = 0; if (_IsBalance(root->_left, leftheight) == false) return false; int rightheight = 0; if (_IsBalance(root->_right, rightheight) == false) return false; height = leftheight > rightheight ? leftheight : rightheight; return abs(leftheight - rightheight) < 2 && (root->_bf == rightheight - leftheight); }
關於完整的AVL樹的代碼,會在下面給出,這裏想多說一點的是,AVL樹是一棵高度平衡的二叉樹,當咱們構建好這樣一棵二叉樹以後,進行查找、插入、刪除相應結點的時候,效率確定是最高的,時間複雜度爲O(logN),但實際應用中,比起和他相似的紅黑樹,AVL的實現難度和因爲AVL樹的高要求(abs(bf) <2)致使的插入結點要屢次調整,AVL樹的使用相對較少。