目錄
一、概念
二、AVL樹的模擬實現
2.1 AVL樹節點定義
2.2 AVL樹的基本結構?
2.3?AVL樹的插入
1. 插入步驟
2. 調節平衡因子
3. 旋轉處理?
?4. 開始插入
2.4 AVL樹的查找
2.5 AVL樹的刪除
1. 刪除步驟
2. 調節平衡因子
3. 旋轉處理
4. 開始刪除
結語
一、概念
二叉搜索樹查找效率為O(logN),但如果數據有序或接近有序二叉搜索樹將退化為單支樹,查找元素相當于在順序表中搜索元素,效率變為O(N),效率低下。
因此,兩位俄羅斯的數學家G.M.Adelson-Velskii和E.M.Landis在1962年發明了一種解決上述問題的方法:當向二叉搜索樹中插入新節點后,如果能保證每個節點的左右子樹高度之差的絕對值不超過1(超過則需要對樹中的節點進行調整),即可降低樹的高度,從而減少平均搜索長度。
AVL樹具有以下特點:
- 它的左右子樹都是AVL樹
- 左右子樹高度之差(簡稱平衡因子)的絕對值不超過1(-1/01)
平衡因子的取值由自己規定,在這里為了模擬實現,規定左子樹的高度在根結點中為負值,右子樹的高度在根結點中為正值,即平衡因子=?右子樹高度 - 左子樹高度。
二、AVL樹的模擬實現
2.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;int _bf; // 該節點的平衡因子AVLTreeNode(const pair<K, V>& kv):_left(nullptr),_right(nullptr),_parent(nullptr),_kv(kv),_bf(0){}
};
2.2 AVL樹的基本結構?
template <class K, class V>
class AVLTree
{typedef AVTreeNode<K, V> Node;
public:bool Insert(const std::pair<K, V>& kv);bool Find(const K& key);bool Erase(const K& key);private:Node* _root = nullptr;
};
2.3?AVL樹的插入
1. 插入步驟
- 按照二叉搜索樹的方式插入新節點
- 調整節點的平衡因子
2. 調節平衡因子
插入節點后,會影響該節點到根節點這一條路徑上節點的平衡因子,因此需要進行迭代,將這一條路徑上的平衡因子更新。
步驟:
第一步:?對插入的節點的父節點進行更新,會發生兩種情況:
- 如果插入的是根節點,則無父節點,不用更新。
- 如果插入的是父節點的左節點,則父節點的平衡因子-1;如果插入的是父節點的右節點,則父節點的平衡因子+1。
第二步:?父節點的平衡因子在插入節點后會有三種變化,也對應著三種措施:
- 父節點的平衡因子變為0,則說明在插入前兩顆子樹高度相差1,插入后整體的高度沒有發生變化,不需要向上繼續調整。
- 父節點的平衡因子變為-1或1,則說明在插入前兩顆子樹高度相等,插入后整體的高度+1,需要對父節點的父節點的平衡因子進行更新,這樣就回到了步驟一。
- 父節點的平衡因子變為-2或2,則說明在插入前兩個子樹高度相差1,且插入的節點為高度較高的那顆子樹的葉子節點,這導致了高度相差變為2,需要進行旋轉處理,降低樹的高度,從而達到平衡整顆樹的要求。
圖解:
第一種情況:
第二種情況:
可以看到這里在更新平衡因子的時候,只會影響節點到根節點路徑上的節點的平衡因子,其它的節點不需要考慮。
第三種情況:
更新平衡因子的時候,發現不滿足規則后,直接停止更新,轉而進行旋轉處理。
3. 旋轉處理?
旋轉有四種情況:
第一種情況:右單旋
新節點插入到較高左子樹的左側,左側的高度變高需要將右邊的調整下來,因此叫做右單旋。
具體操作:
讓subL的右孩子成為parent的左孩子,然后讓parent成為subL的右孩子,最后把兩個節點的平衡因子修改為0。
?圖解:
實現代碼:
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 1.先讓把subL的右邊作為parent的左邊parent->_left = subLR;// 2.如果subLR不為空,就讓subLR的父指針指向parentif (subLR) subLR->_parent = parent;// 3.記錄parent的父節點的位置,然后把parent作為subL的右邊Node* pParent = parent->_parent;subL->_right = parent;// 4.parent的父指針指向subLparent->_parent = subL;// 5.如果ppNode為空,說明subR現在是根節點,就讓subL的父指針指向nullptr// 不是根節點就把subL的父指針指向parent的父節點,parent的父節點(左或右)指向subLif (pParent == nullptr){// 更新根節點_root = subL;subL->_parent = nullptr;}else{// 判斷parent是pparent的左還是右if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;subL->_parent = pParent;}// 6.把parent和subL的平衡因子更新為0subL->_bf = parent->_bf = 0;
}
第二種情況:左單旋?
新節點插入到較高右子樹的右側,右側的高度變高需要將左邊的調整下來,因此叫做右單旋。
?具體操作:
讓subR的左孩子成為parent的右孩子,然后讓parent成為subR的左孩子,最后把兩個節點的平衡因子修改為0。
圖解:
實現代碼:
void RotateL(Node* parent)
{ Node* subR = parent->_right;Node* subRL = subR->_left;// 1.先讓把subR的左邊作為parent的右邊parent->_right = subRL;// 2.如果subRL不為空,就讓subRL的父指針指向parentif (subRL) subRL->_parent = parent;// 3.先記錄parent的父節點的位置,然后把parent作為subR的左邊 Node* pParent = parent->_parent;subR->_left = parent;// 4.parent的父指針指向subRparent->_parent = subR;// 5.如果ppNode為空,說明subR現在是根節點,就讓subR的父指針指向nullptr// 不是根節點就把subR的父指針指向parent的父節點,parent的父節點(左或右)指向subRif (pParent == nullptr){// 更新根節點_root = subR;subR->_parent = nullptr;}else{// 判斷parent是ppNode的左還是右if (pParent->_left == parent)pParent->_left = subR;elsepParent->_right = subR;subR->_parent = pParent;}// 6.把parent和subR的平衡因子更新為0subR->_bf = parent->_bf = 0;
}
第三種情況:左右雙旋?
新節點插入在較高左子樹右側,這里和第一種情況的區別就是前者是直線,后者是折線
具體操作:
先對subL進行一個左單旋,然后對parent進行右單旋,修改平衡因子,有三種改法。三個節點從左至右的三個節點一次是:subL、subLR和parent。
如果subLR的平衡因子為0,就將它們依次改為0,0, 0;
如果subLR的平衡因子為1,就將它們依次改為-1,0, 0;
如果subLR的平衡因子為-1,就將它們依次改為0,0, 1。
圖解:(這里只畫出了一種情況,剩下的類似)
實現代碼:
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;// 保留subLR的平衡因子的值,方便知道新插入的節點是在subLR的左子樹還是右子樹// 旋轉 先對subL進行左旋轉,再對parent進行右旋轉RotateL(subL);RotateR(parent);// 從左到右 subL subLR parentif (bf == -1)// subLR的左子樹 bf: 0 0 1{subL->_bf = 0;subLR->_bf = 0;parent->_bf = 1;}else if (bf == 1)// subLR的右子樹 bf: -1 0 0{subL->_bf = -1;subLR->_bf = 0;parent->_bf = 0;}else if (bf == 0){subL->_bf = 0;subLR->_bf = 0;parent->_bf = 0;}
}
第四種情況:右左單旋?
新節點插入在較高右子樹左側,這里和第二種情況的區別就是前者是直線,后者是折線
?具體操作:
先對subR進行一個右單旋,然后對parent進行左單旋,修改平衡因子,有三種改法。三個節點從左至右的三個節點一次是:parent、subRL和subR。
如果subRL的平衡因子為0,就將它們依次改為0,0, 0;
如果subRL的平衡因子為1,就將它們依次改為-1,0, 0;
如果subRL的平衡因子為-1,就將它們依次改為0,0, 1。
圖解:(這里只畫出了一種情況,剩下的類似)
實現代碼:
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;// 保留subRL的平衡因子的值,方便知道新插入的節點是在subRL的左子樹還是右子樹// 旋轉 先對subR進行右旋轉,再對parent進行左旋轉RotateR(subR);RotateL(parent);// 從左到右 parent subRL subRif (bf == -1)// subRL的左子樹 bf: 0 0 1{parent->_bf = 0;subRL->_bf = 0;subR->_bf = 1;}else if (bf == 1)// subRL的右子樹 bf: -1 0 0{parent->_bf = -1;subRL->_bf = 0;subR->_bf = 0;}else if (bf == 0){parent->_bf = 0;subRL->_bf = 0;subR->_bf = 0;}
}
?4. 開始插入
bool Insert(const pair<K, V>& kv)
{// 先按照二叉搜索數一樣插入元素// 無節點是插入if (_root == nullptr){_root = new Node(kv);return true;}// 有節點時插入Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;// 小于往左走if (kv.first < cur->_kv.first){cur = cur->_left;}// 大于往右走else if (kv.first > cur->_kv.first){cur = cur->_right;}else{// 找到了,就返回falsereturn false;}}cur = new Node(kv);// 判斷cur應該插在parent的左還是右 // 小于在左,大于在右 if (cur->_kv.first < parent->_kv.first){parent->_left = cur;cur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}// 更新parent的平衡因子// 節點的插入只會影響cur的祖先的平衡因子(不是所有的,是一部分,分情況)while (parent){// 更新parent的平衡因子// cur在parent的左,parent->_bf--// cur在parent的右,parent->_bf++if (cur == parent->_left)parent->_bf--;elseparent->_bf++;// bf 可能為 -2、-1、0、1、2if (parent->_bf == 0){// 對上層無影響,退出break;}else if (parent->_bf == -1 || parent->_bf == 1){// 對上層有影響,迭代更新cur = parent;parent = parent->_parent;}else{// 平衡樹出現了問題,需要調整// 1.右邊高,左旋轉調整if (parent->_bf == 2){// 如果是一條直線==>左旋轉即可// 如果是一條折線==>右左旋轉if (cur->_bf == 1)RotateL(parent);else if (cur->_bf == -1)RotateRL(parent);}// 2.左邊高,右旋轉調整else if (parent->_bf == -2){// 如果是一條直線==>右旋轉即可// 如果是一條折線==>左右旋轉if (cur->_bf == -1)RotateR(parent);else if (cur->_bf == 1)RotateLR(parent);}// 調整后是平衡樹,bf為0,不需要調整了break;}}return bool;
}
2.4 AVL樹的查找
與二叉搜索樹的過程一致,這里就不多解釋了,直接上代碼:
bool Find(const K& key)
{if (_root == nullptr)return false;Node* cur = _root;while (cur){// 小于往左走if (key < cur->_kv.first){cur = cur->_left;}// 大于往右走else if (key > cur->_kv.first){cur = cur->_right;}else{// 找到了return true;}}return false;
}
2.5 AVL樹的刪除
1. 刪除步驟
- 我們先按照二叉搜索樹樹刪除節點的方式,刪除節點。
- 根據對應刪除情況更新平衡因子,更新平衡因子的方法與插入的更新方法是相反的。
2. 調節平衡因子
步驟:
第一步:
- 刪除節點后,如果刪除的節點為根節點,就結束。
- 如果刪除節點是父節點的左孩子,那么父節點的平衡因子加1;刪除節點是父節點的右孩子,那么父節點的平衡因子減1。
第二步:父節點的平衡因子在插入節點后會有三種變化,也對應著三種措施:
- 父節點的平衡因子變為0,則說明刪除前父親的平衡因子為1或-1,多出一個左節點或右節點,刪除節點后,左右高度相等,整體高度減1,對上層有影響,需要繼續調節。
- 父節點的平衡因子變為-1或1,則說明刪除前父親的平衡因子為0,左右高度相等,刪除節點后,少了一個左節點或右節點,但是整體高度不變,對上層無影響,不需要繼續調節。
- 父節點的平衡因子變為-2或2,則說明刪除前父親的平衡因子為-1或1,多了一個左節點或一個右節點,刪除了一個右節點或左節點,此時多了兩個左節點和右節點,這棵子樹一邊已經被拉高了,此時這棵子樹不平衡了,需要旋轉處理。
3. 旋轉處理
這里的旋轉與插入時旋轉的沒有區別,就不詳細解答了,直接上代碼。
4. 開始刪除
bool Erase(const K& key)
{if (_root == nullptr)return false;// 有節點時插入Node* parent = nullptr;Node* cur = _root;while (cur){// 小于往左走if (key < cur->_kv.first){parent = cur;cur = cur->_left;}// 大于往右走else if (key > cur->_kv.first){parent = cur;cur = cur->_right;}else{// 找到了// 1.左邊為空,把parent指向cur的右// 2.右邊為空,把parent指向cur的左// 3.左右都不為空,用右子樹的最左邊的節點的值替換要刪除的節點,然后轉換為用1的情況刪除該節點if (cur->_left == nullptr){if (cur == _root){_root = cur->_right;delete cur;break;}else{if (parent->_left == cur){parent->_left = cur->_right;parent->_bf++;}else{parent->_right = cur->_right;parent->_bf--;}}if (parent->_bf != -1 && parent->_bf != 1) UpdateBf(parent);delete cur;}else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;delete cur;break;}else{if (parent->_left == cur){parent->_left = cur->_left;parent->_bf++;}else{parent->_right = cur->_left;parent->_bf--;}}if (parent->_bf != -1 && parent->_bf != 1) UpdateBf(parent);delete cur;}else{Node* rightMinParent = cur;Node* rightMin = cur->_right;// 先去右子樹while (rightMin->_left){rightMinParent = rightMin;rightMin = rightMin->_left;// 一種往左走}cur->_kv = rightMin->_kv;// 替代刪除// 刪除minNode 第一種情況:左節點為空if (rightMinParent->_left == rightMin){rightMinParent->_left = rightMin->_right;rightMinParent->_bf++;}else{rightMinParent->_right = rightMin->_right;rightMinParent->_bf--;}if (rightMinParent->_bf != -1 && rightMinParent->_bf != 1) UpdateBf(rightMinParent);delete rightMin;}return true;}}return false;
}void UpdateBf(Node* parent)
{if (parent == nullptr)return;Node* cur = parent;goto first;while (parent){// 更新parent的平衡因子// cur在parent的左,parent->_bf++// cur在parent的右,parent->_bf--if (cur == parent->_left)parent->_bf++;elseparent->_bf--;// bf 可能為 -2、-1、0、1、2first:if (parent->_bf == 0){// 對上層有影響,迭代更新// 如果parent是根節點就結束if (parent->_parent == nullptr)break;cur = parent;parent = parent->_parent;}else if (parent->_bf == -1 || parent->_bf == 1){// 對上層無影響,退出break;}else{// 平衡樹出現了問題,需要調整// 1.右邊高,左旋轉調整if (parent->_bf == 2){// 如果是一條直線==>左旋轉即可// 如果是一條折線==>右左旋轉if (parent->_right->_bf == 1){RotateL(parent);cur = parent->_parent;parent = cur->_parent;continue;}else if (parent->_right->_bf == -1)// 調整后 p sL s 如果sL是1或-1可以退出 {Node* s = parent->_right;Node* sL = s->_left;RotateRL(parent);// 不平衡向上調整if (sL->_bf != 1 && sL->_bf != -1){cur = sL;parent = cur->_parent;continue;}}else if (parent->_right->_bf == 0) // 平衡因子修改{RotateL(parent);parent->_bf = 1;parent->_parent->_bf = -1;}}// 2.左邊高,右旋轉調整else if (parent->_bf == -2){// 如果是一條直線==>右旋轉即可// 如果是一條折線==>左右旋轉if (parent->_left->_bf == -1){RotateR(parent);cur = parent->_parent;parent = cur->_parent;continue; //parent不是-1或1就繼續}else if (parent->_left->_bf == 1)// 調整后 s sR p 如果sL是1或-1可以退出{Node* s = parent->_left;Node* sR = s->_right;RotateLR(parent);// 不平衡向上調整 為0時如果parent為根//if (sR->_bf != 1 && sR->_bf != -1){cur = sR;parent = cur->_parent;continue;}}else if (parent->_left->_bf == 0) // 平衡因子修改{RotateR(parent);parent->_parent->_bf = 1;parent->_bf = -1;}}// 調整后是平衡樹,bf為1或-1,不需要調整了break;}}
}
結語
上面這些是AVL樹的大致內容,其中旋轉是有些難的地方,但是面試會考察,需要著重掌握,而刪除是二叉搜索樹的刪除加上旋轉的疊加,難度更上一層了,這里如果沒能理解,可以自己畫一畫圖,并且配合著插入的圖來分析,應該會有所幫助。
下一篇將會介紹二叉搜索樹的另一種改良:紅黑樹,有興趣的朋友可以關注一下。