数据结构-AVL树
文章目錄
- 一:AVL樹的概念
- 二:AVL樹實現
- (1)AVL樹結點的定義
- (2)AVL樹的插入
- A:平衡因子更新
- B:AVL樹的旋轉
- 第一類:單旋
- 第二類:雙旋
- C:判斷是否是平衡二叉樹
- (3)AVL樹刪除
一:AVL樹的概念
普通的二叉搜索樹有一個致命的缺陷,就是如果將有序序列插入樹中,樹的高度會不斷增大,我們知道二叉搜索樹樹的高度將直接決定搜索效率。
極端情況下將退化為單支樹
因此如果能保證每個結點的左右子樹的高度之差的絕對值不超過1,就能降低樹的高度,從而提高搜索效率,我們把這種結構稱之為高度平衡搜索樹,簡稱為AVL樹
下面的這棵樹就是一棵AVL樹,每個結點的平衡因子(平衡因子就是該結點左右子樹高度值差的絕對值)
二:AVL樹實現
(1)AVL樹結點的定義
template<class K, class V> struct AVLTreeNode {AVLTreeNode<K, V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;//可以有pair<K, V> _kv;AVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0)//剛創建的結點平衡因子為0{}int bf;//平衡因子};(2)AVL樹的插入
AVL樹本質也是一個二叉搜索樹,所以在插入上,其邏輯和二叉搜索樹的并無很大區別
pair<Node*, bool> insert(const pair<K, V>& kv) {//1:AVL樹插入if (_root == nullptr){_root = new Node(kv);return pair(_root, true);}Node* parent = nullptr;Node* cur = _root;while (cur){if (kv.first > cur->_kv.first)//插入的結點大于該結點,向右尋找{cur = cur->_right;if (cur == nullptr)//如果某次尋找中cur為空,表示找到插入位置{parent->_right = new Node(kv);//cur的父節點申請結點cur = parent->_right;cur->_parent = parent;//然后把該結點的父節點指針指向父節點}parent = cur;}else if (kv.first < cur->_kv.first){cur = cur->_left;if (cur == nullptr){parent->_left = new Node(kv);cur = parent->_left;cur->_parent = parent;}parent = cur;}else//沒有找到,返回false及相同的值的位置{return pair<cur, false>;}}//2:AVL樹平衡因子調整//見下面return pair<cur, true>; }A:平衡因子更新
由于是AVL樹,所以在插入時要時刻保證樹的平衡,對于每個新插入的結點,由于左右子樹為空,所以對于這個單個結點的樹來說它肯定是平衡的,其平衡因子是0,但是如果它作為了其他結點的子樹,就有可能影響其他結點的平衡因子,這種影響甚至有可能一直傳達至祖先結點
新增結點會影響它的祖先的平衡因子
-
1:如果新增結點cur作為了parent結點的左子樹,那么parent的平衡因子-1
-
上圖中在結點8的左子樹處插入了一個結點,導致結點8的平衡因子-1變為了0,此時本應該繼續向上調整(因為8的改變有可能會影響到它的祖先),但是它變化為了0,所以這種變化恰好“抵消了”,因此可以停止更新而不用繼續判斷祖先是否受影響
-
2:如果新增結點cur作為了parent結點的右子樹,那么parent的平衡因子+1
-
上圖在9這個結點的右子樹處插入了一個結點,因此9這個節點的平衡因子+1,變為1,此時結點9的平衡因子沒有變成0,代表它的改變對上層可能會產生影響,因此要繼續向上判斷,對于結點8,其原來的平衡因子是1,所以此時對于它來說就應該調整為2,而當平衡因子大于等于2時,代表此樹不平衡,所以要立即進行旋轉操作,以保證樹的平衡
除旋轉代碼外,平衡因子的調整代碼如下,其中平衡因子為0和平衡因子為1或-1的情況就分別對應上面圖中的例子。
//1:AVL插入//見上面Node* ret = cur;//保存cur,因為下面的調整過程,會改變cur//2:AVL樹調整平衡因子while (parent)//最厲害,調整到根節點。和堆的向上調整算法有點類似{//首先需要判斷cur這個結點的插入對parent的影響if (cur == parent->_right)parent->_bf++;//如果插入到了右面,平衡因子+1elseparent->_bf--;//如果插入到了左面,平衡因子-1if (parent->_bf == 0)//如果插入之后平衡因子為0,表示原來可能平衡因子1或-1,但是插入后抵消了變化break;//如果這樣就不需要調整了else if (abs(parent->_bf) == 1)//如果插入后平衡因子為1或-1,表示原來是0,但是插入后變化了{cur = parent;parent = parent->_parent;//如果這樣,這個結點有可能影響其父節點,所以要迭代判斷父節點的情況}else if (abs(parent->_bf) == 2)//如果為2,表示樹已經不平衡了,需要進行旋轉{//旋轉代碼}elseassert("平衡因子非法");}B:AVL樹的旋轉
AVL樹的旋轉共分為兩類四種。另外旋轉方式的命名可能從感覺上來講有點別扭,其實旋轉方式的命名時對不平衡狀態的描述而不是對調整過程的描述
第一類:單旋
1:右單旋轉調整(LL):新節點插入到了較高左子樹的左側
下圖中為抽象樹,三角形表示的樹為高度平衡的二叉樹搜索樹。在下面這個情況中,結點A的平衡因子絕對值為1,左子樹較高
現在進行插入,來了一個新的結點恰好插入到了B結點的左樹,導致A結點的平衡因子變為2,樹不平衡,需要進行調整
調整時:將結點A下移一個高度,B上移一個高度。也就是將B從A的左子樹處取下,然后將B的右子樹掛在a的左子樹處(為什么這樣做?因為這樣做是符合二叉搜索樹的特性的),最后將A掛在B的右子樹處
整個過程似乎很簡單,但是其代碼卻有很多需要注意的地方,一旦不注意極易寫出bug,我們這里使用的是三叉鏈,首先代碼的大題邏輯如下
- 即把30(30作為的60的左結點)的右節點掛在60的左節點處,然后把60結點掛在30結點右節點處
因此首先定義兩個節點subL和subLR,含義如下
接著60的left指向subLR,注意判空,因為subLR可能為空
parent->_left = subLR;//parent的左孩子此時要指向subLR if (subLR)subLR->_parent = parent;//更新_parent,需要注意判斷,因為subLR有可能是空的接著對于parent來說,它也有自己的父節點,調整之后,parent的父節點的孩子將不再是parent,而是subL了,因此首先用一個結點保存parent的父節點
Node* parent_parent = parent->_parent;//用來保存parent的parent結點此時30將作為新的parent,那么它的right就會指向60,也就是parent
subL->_right = parent;//subL的right指向parent parent->_parent = subL;//更新_parent注意判斷一個特殊情況,因為60也就是parent有可能就是根節點,如果是這樣的話直接改變將導致樹丟失
if (parent == _root)//一個非常特殊的情況,假如parent就是根節點{//subL成為了新的根節點_root = subL;_root->_parent = nullptr;}else//正常情況{}如果是正常情況,那么就修改parent的父節與新的parent(也就是subL)的關系即可
if (parent == _root)//一個非常特殊的情況,假如parent就是根節點{//subL成為了新的根節點_root = subL;_root->_parent = nullptr;}else//正常情況{//如果parent是parent的父節點的左孩子if (parent_parent->_left = parent)parent_parent->_left = subL;//那么parent的left就要指向新的parent(subL)elseparent_parent->_right = subL;subL->_parent = parent_parent;}因此完整代碼如下
void LL(Node* parent)//右單旋轉調整 {Node* subL = parent->_left;//parent的左孩子Node* subLR = subL->_right;//parent的左孩子的右孩子parent->_left = subLR;//parent的左孩子此時要指向subLRif (subLR)subLR->_parent = parent;//更新_parent,需要注意判斷,因為subLR有可能是空的Node* parent_parent = parent->_parent;//用來保存parent的parent結點subL->_right = parent;//subL的right指向parentparent->_parent = subL;//更新_parentif (parent == _root)//一個非常特殊的情況,假如parent就是根節點{//subL成為了新的根節點_root = subL;_root->_parent = nullptr;}else//正常情況{//如果parent是parent的父節點的左孩子if (parent_parent->_left = parent)parent_parent->_left = subL;elseparent_parent->_right = subL;subL->_parent = parent_parent;}subL->_bf = parent->_bf = 0;//處理平衡因子 }2:左單旋轉調整(RR):新節點插入到了較高右子樹的右側
RR調整和LL調整情況恰好相反,調整時節點B上移,節點A下移,讓節點A做節點B的左子樹,讓節點B的左子樹做節點A的右子樹
左單旋轉調整和右單旋轉調整代碼基本一致,只是邏輯相反
第二類:雙旋
1:先左后右雙旋轉調整(LR):新節點插入到了較高左子樹的右側
首先是旋轉的基本過程
在LL調整中,新的結點插入到了較高左子樹的左側,調整后可以使樹平衡
但是如果此時將新節點插入到較高左子樹的右側
如果繼續使用LL調整,會發現怎么也調整不過去,依然不平衡
在這種情況下就要使用到雙旋轉調整了
我們先把較高左子樹的右子樹拆分一下,拆分為兩棵樹
大家會發現此時如果要在B的右側插入一個節點就有兩個選擇——要么插入到C的左側要么插入到C的右側
這里我以插入到C的右側為例
接著進行雙旋轉調整:先對B樹進行左單旋轉
形成了這樣一顆樹
然后對這顆樹進行右單旋轉調整,樹就平衡了
- 本例是插入到了C的右側,如果插入到了C的左側,調整也是一樣的
接著是代碼部分
根據之前的描述,代碼部分的邏輯也應該是先左旋后右旋即可,也就是直接調用前面的接口。
但是這里面最難處理的是平衡因子的問題,因為在上面的例子中雖然插入到C左側和右側采用的都是LR調整,但是調整完成之后,大家也發現了,新插入的結點會對平衡因子產生不同的影響
如下是不同插入情況下,平衡因子值的不同情況。其中C由于一定會到“根節點”位置,所以它的平衡因子始終為0
所以這份代碼中我們首先依然要定義subL和subLR,還要實現保存subLR的平衡因子,然后讓其LR調整
左單和右單旋轉調整接口中我們最終都干了這樣的一個操作
subL->_bf = parent->_bf = 0;//處理平衡因子 subR->_bf = parent->_bf = 0;//處理平衡因子所以在LR調整結束之后,根據之前保存的平衡因子,修改為上圖中所展示的情況即可
- 如果是插入到了C的右面
- 如果是插入到了C的左面
- 如果subLR的平衡因子為0,說明subLR本身就是新增的結點,直接歸0即可(其實這個操作不用也行,因為在兩個接口中也會處理)
代碼如下
void LR(Node* parent)//先左后右雙旋轉調整 {Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RR(parent->_left);LL(parent);//先左后右if (bf == 1)//subLR插入到了右面{subLR->_bf = 0;parent->_bf = 0;subL->_bf = -1;}else if (bf == -1)//subLR插入到了左面{subLR->_bf = 0;subL->_bf = 0;parent->_bf = 0;}else//subLR本身就是新增結點{subLR->_bf = subL->_bf = parent->_bf = 0;} }2:先右后左雙旋轉調整(RL):新節點插入到了較高左子樹的左側
在左單(RR)旋轉調整中,面對的情況是新節點插入到了較高右子樹的右側,而如果新節點插入到了較高右子樹的左側,那么就要使用先右后左雙旋轉調整
所以在這種情況下,先對右子樹進行右單旋轉調整,然后再進行左單旋轉調整
代碼邏輯也會前面的完全相反,這里就不過多闡述了
至此,寫完這四種調整之后,我們就可以通過平衡因子,在插入代碼中進行判斷,去選擇相應的旋轉方式
代碼如下
- 注意不要忘記再調整完成之后要跳出,因為調整最終的結果就是平衡
C:判斷是否是平衡二叉樹
代碼如下
int Height(Node* root)//求樹的高度 {if (root = nullptr)return 0;int left = Height(root->_left);int right = Height(root->_right);return left > right ? left + 1 : right + 1; }bool _Isbalance(Node* root) {if (root == nullptr)return true;int left_high = Height(root->_left);int right_high = Height(root->_right);if (right_high - left_high != root->_bf){cout << "平衡因子錯誤:" << root->_kv.first << endl;}return abs(left_high - right_high) < 2 && _Isbalance(root->_left) && _Isbalance(root->right); } bool _Isbalance()//判斷是否是平衡樹,順便檢查平衡因子更新的是否正確 {return _Isbalance(_root); }(3)AVL樹刪除
了解即可。AVL樹的刪除遵從二叉搜索樹刪除的細則,但是需要注意的是刪除時也要更新平衡因子,以及不平衡時進行旋轉。
具體相關細則可以
總結
- 上一篇: SPI
- 下一篇: Javascript 面向对象编程初探(