目录
4.3 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
4.4 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
0. 前言
前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个 共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此 map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
本篇文章主要分析AVL树插入的实现、及算法思想,分别针对各种情况采取的平衡调节分析,对于AVL树的删除操作,由于其复杂度和使用场景,不涉及分析,重点在后续的红黑树!
1. AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整)。
即可降低树的高度,从而减少平均搜索长度。 一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 $O(log_2 n)$,搜索时间复杂度O($log_2 n$)。
2. AVL树节点的定义
AVL树因为其需要为后续旋转调整平衡操作的便捷,采用三叉链实现,通过父节点指针,便可实现向上遍历,同时使用balance factor记录每个节点的平衡因子,数据存储采取STL提供的pair类实现的Key-Value模型!
template<class K,class V> struct AVLTreeNode { AVLTreeNode<K, V>* _left; AVLTreeNode<K, V>* _right; AVLTreeNode<K, V>* _parent; std::pair<K, V> _kv; int _bf; //balance factor 平衡因子 AVLTreeNode(const std::pair<K,V>& kv) :_left(nullptr) ,_right(nullptr) ,_parent(nullptr) ,_kv(kv) ,_bf(0) {} };
3. AVL树的插入
AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么 AVL树的插入过程可以分为两步:
- 按照二叉搜索树的方式插入新节点
- 调整节点的平衡因子
template<class K, class V> class AVLTree { typedef AVLTreeNode<K, V> Node; public: bool Insert(const std::pair<K, V>& kv) { // 1. 先按照二叉搜索树的规则将节点插入到AVL树中 if (_root == nullptr) { _root = new Node(kv); return true; } Node* parent = nullptr; Node* cur = _root; while (cur != nullptr) { if (cur->_kv.first < kv.first) { parent = cur; cur = cur->_right; } else if (cur->_kv.first > kv.first) { parent = cur; cur = cur->_left; } else { return false; } } cur = new Node(kv); if (parent->_kv.first < kv.first) { parent->_right = cur; } else { parent->_left = cur; } cur->_parent = parent; // 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否破坏了AVL树的平衡性 //控制平衡_判断是否平衡_更新平衡因子 /*pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent 的平衡因子分为三种情况: - 1,0, 1, 分以下两种情况: 1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子 - 1即可 2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子 + 1即可 此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2 1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整 成0,此时满足AVL树的性质,插入成功 2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更 新成正负1,此时以pParent为根的树的高度增加,需要继续向上更新 3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进 行旋转处理*/ while (parent) { if (cur == parent->_right) { parent->_bf++; } else { parent->_bf--; } // 更新后检测双亲的平衡因子 if (parent->_bf == 0) { //平衡状态 return true; } else if (abs(parent->_bf) == 1) { //parent高度变高,继续向上更新高度 cur = parent; parent = parent->_parent; } else if (abs(parent->_bf) == 2) { //说明parent所在子树,不平衡,需要旋转 //情况1:右右 —— 左旋转 if (parent->_bf == 2 && cur->_bf == 1) { RotateL(parent); } //情况2:左左 —— 右旋转 else if (parent->_bf == -2 && cur->_bf == -1) { RotateR(parent); } else if (parent->_bf == -2 && cur->_bf == 1) { RotateLR(parent); } else if (parent->_bf == 2 && cur->_bf == -1) { RotateRL(parent); } else { assert(false); } break; } else { //插入前就不是AVL树 assert(false); } } return true; } private: Node* _root = nullptr; };
4. AVL树的旋转
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构, 使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
4.1 新节点插入较高右子树的右侧---右右:左单旋
当新增节点导致平衡因子向上调整时,如果cur ->_bf == 1 && parent->_bf == 2,此时parent->_bf == 2说明右子树为最高子树,cur ->_bf == 1说明右子树的右树为插入位置,需要进行左单旋,绘图分析情况:
first:
second:
third:
代码实现:
//时间复杂度O(1),且更新因子最多走高度次 void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; subR->_left = parent; //parent是整棵树的根 //parent是子树的根,需要更改根的父亲的左或右节点 Node* pParent = parent->_parent; if (parent == _root) { _root = subR; } else { if (parent == pParent->_left) { pParent->_left = subR: } else { pParent->_right = subR; } } subR->_parent = pParent; parent->_parent = subR; parent->_right = subRL; if (subRL) { subRL->_parent = parent; } subR->_bf = parent->_bf = 0; }
4.2 新节点插入较高左子树的左侧---左左:右单旋
当新增节点导致平衡因子向上调整时,如果cur ->_bf == -1 && parent->_bf == -2,此时parent->_bf == -2说明左树为最高子树,cur ->_bf == -1说明左子树的左树为其插入位置,需要进行右单旋,绘图分析情况:
上述后情况和左单旋后续分析近似
代码实现:
void RotateR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; Node* pParent = parent->_parent; subL->_right = parent; parent->_parent = subL; if (parent == _root) { _root = subL; } else { if (pParent->_left == parent) { pParent->_left = subL; } else { pParent->_right = subL; } } subL->_parent = pParent; parent->_left = subLR; if (subLR) { subLR->_parent = parent; } subL->_bf = parent->_bf = 0; }
4.3 新节点插入较高左子树的右侧---左右:先左单旋再右单旋
当新增节点导致平衡因子向上调整时,如果cur ->_bf == 1 && parent->_bf == -2,此时说明左树为最高子树,且插入在左子树的右树,为其插入位置,绘图分析情况:
代码实现:
void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf; RotateL(parent->_left); RotateR(parent); subLR->_bf = 0; if (bf == 1) { parent->_bf = 0; subL->_bf = -1; } else if (bf == -1) { parent->_bf = 1; subL->_bf = 0; }else if(bf == 0){ parent->_bf = 0; subL->_bf = 0; } else { assert(false); } }
4.4 新节点插入较高右子树的左侧---右左:先右单旋再左单旋
当新增节点导致平衡因子向上调整时,如果cur ->_bf == -1 && parent->_bf == 2,此时说明右子树为最高子树,且插入在右子树的左树,为其插入位置,绘图分析情况:
代码实现:
void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf; RotateR(parent->_right); RotateL(parent); subRL->_bf = 0; if (bf == 1) { parent->_bf = -1; subR->_bf = 0; } else if (bf == -1) { parent->_bf = 0; subR->_bf = 1; } else if (bf == 0) { parent->_bf = 0; subR->_bf = 0; } else { assert(false); } }
4.5 AVL旋转总结
假如以pParent为根的子树不平衡,即pParent的平衡因子为2或者-2,分以下情况考虑:
1. pParent的平衡因子为2,说明pParent的右子树高,设pParent的右子树的根为pSubR
- 当pSubR的平衡因子为1时,执行左单旋
- 当pSubR的平衡因子为-1时,执行右左双旋
2. pParent的平衡因子为-2,说明pParent的左子树高,设pParent的左子树的根为pSubL
- 当pSubL的平衡因子为-1是,执行右单旋
- 当pSubL的平衡因子为1时,执行左右双旋 旋转完成后,
原pParent为根的子树个高度降低,已经平衡,不需要再向上更新。
5. AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:
1. 验证其为二叉搜索树
- 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
- 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
- 节点的平衡因子是否计算正确
代码实现:
public: void Inorder() { _Inorder(_root); } bool IsBalance() { return _IsBalance(_root); } private: bool _IsBalance(Node* cur) { if (cur == nullptr) { return true; } int diff = _Height(cur->_right) - _Height(cur->_left); if (diff != cur->_bf) { std::cout << cur->_kv.first << "平衡因子异常" << std::endl; return false; } return (abs(diff) < 2) && \ _IsBalance(cur->_left) && _IsBalance(cur->_right); } int _Height(Node* cur) { if (cur == nullptr) { return 0; } return std::max(_Height(cur->_left), _Height(cur->_right)) + 1; } void _Inorder(Node* cur) { std::stack<Node*> st; while (cur || !st.empty()) { while (cur) { st.push(cur); cur = cur->_left; } cur = st.top(); std::cout << cur->_kv.second << " "; st.pop(); cur = cur->_right; } std::cout << std::endl; } //时间复杂度O(1),且更新因子最多走高度次 void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; Node* pParent = parent->_parent; subR->_left = parent; if (parent == _root) { _root = subR; } else { if (pParent->_left == parent) { pParent->_left = subR; } else { pParent->_right = subR; } } subR->_parent = pParent; parent->_parent = subR; parent->_right = subRL; if (subRL) { subRL->_parent = parent; } subR->_bf = parent->_bf = 0; }
3. 验证用例
void TestAVLTree1() { //int a[] = { 16,3,7,11,9,26,18,14,15 }; int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 }; AVLTree<int, int> t1; for (auto e : a) { t1.Insert(std::make_pair(e, e)); } t1.Inorder(); std::cout << t1.IsBalance() << "\n"; } void TestAVLTree2() { size_t N = 10000; srand((unsigned int)time(nullptr)); AVLTree<int, size_t> t1; for (size_t i = 0; i < N; ++i) { int x = rand(); t1.Insert(std::make_pair(x, i)); } std::cout << t1.IsBalance() << "\n"; }
6. AVL树的删除
因为AVL树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不 错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
具体实现学生们可参考《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版。
7. AVL树的性能
AVL树是一棵高度平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。