当前位置:   article > 正文

数据结构——AVL树

数据结构——AVL树

什么是AVL树

先来回顾一下二叉搜索树,二叉搜索树虽然可以缩短查找的效率,但是如果数据有序或者接近有序的话,二叉搜索树就会退化成单支树,查找元素相当于在顺序表中搜索元素,效率非常低。因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新的结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一颗AVL树或者是空树,或者是具有以下性质的二叉搜索树:

1.它的左右子树都是AVL树。

2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。

例如下图:如果一颗二叉搜索树是高度平衡的,它就是AVL树。如果它有N个结点,其高度可保持在O(logN),搜索时间复杂度为O(lonN)。

bce2555267694964a730906370a17ba3.png

AVL树结点的定义

在结点的定义中,我们直接定义成KV模型的结点,并新增一个平衡因子,还有一个_parent指针,也就是说,该结点拥有三个指针+一对键值对+一个平衡因子,在初始化时指针都初始化为空,平衡因子设定成0。

  1. template<class K,class V>
  2. struct AVLTreeNode
  3. {
  4. AVLTreeNode<K, V>* _left; //左子树指针
  5. AVLTreeNode<K, V>* _right;//右子树指针
  6. AVLTreeNode<K, V>* _parent;//每个结点都包含了一个父结点地址
  7. pair<K, V> _kv;//键值对
  8. int _bf;//平衡因子
  9. //构造函数
  10. AVLTreeNode(const pair<K, V>& kv)
  11. :_left(nullptr)
  12. , _right(nullptr)
  13. , _parent(nullptr)
  14. , _kv(kv)
  15. , _bf(0)
  16. {}
  17. };

AVL树的插入和平衡因子

首先为什么结点已经有了_parent,为什么还要新定义一个parent呢?这两个的含义不同,_parent的是每个结点都含有对父结点的链接,而parent则是定义成cur指针的父结点指针,也就是随时会发生变动,容易处理。也是为了和二叉搜索树的cur parent配合。

AVL树的插入:在AVL树的插入中,和二叉搜索树的插入相同,这里就不多赘述了。

AVL树的平衡因子:在前面可以知道,新插入的结点的平衡因子都初始化成0,那么该结点的父结点就需要进行更改了。

步骤一:更改平衡因子

1.如果cur插入在parent的左边,那么parent的平衡因子-1

2.如果cur插入在parent的右边,那么parent的平衡因子+1

步骤二:检查平衡因子

1.如果parent的平衡因子为0,那么说明插入前为±1,那么说明此时插入后不会增加高度。

2.如果parent的平衡因子为±1,那么说明插入前为0,那么说明此时插入后会增加高度。那么此时需要对祖先结点进行更新,让cur和parent各自返回上一层,然后重复步骤一和步骤二进行循环。

3.如果parent的平衡因子为±2,那么说明插入后违反了AVL树的性质,需要旋转处理。

  1. //插入函数
  2. bool insert(const pair<K,T>& kv)
  3. {
  4. //按照二叉树搜索树插入
  5. if (_root == nullptr)//根结点为空时new一个最初的根结点
  6. {
  7. _root = new Node(kv);
  8. return true;
  9. }
  10. Node* parent = nullptr;//这个为当前指针cur的父结点指针
  11. Node* cur = _root;//当前指针指向根
  12. while (cur)//当不为空,说明存在值,那么继续搜索可插入的地方
  13. {
  14. if (cur->_kv.first < kv.first)//key大于结点值,往右走
  15. {
  16. parent = cur;
  17. cur = cur->_right;
  18. }
  19. else if (cur->_kv.first > kv.first)//key小于结点值,往左走
  20. {
  21. parent = cur;
  22. cur = cur->_left;
  23. }
  24. else//相等,那么不插入,插入失败
  25. {
  26. return false;
  27. }
  28. }
  29. //找到地方插入,new一个新结点
  30. cur = new Node(kv);
  31. if (parent->_kv.first < kv.first)//key大于父结点值,插右边
  32. {
  33. parent->_right = cur;
  34. cur->_parent = parent;
  35. }
  36. else//小于那么插左边
  37. {
  38. parent->_left = cur;
  39. cur->_parent = parent;
  40. }
  41. return true;//插入成功
  42. //开始更新平衡因子
  43. while (cur != _root)//最差的情况就是一直更新回根结点
  44. {
  45. //处理因子
  46. if (cur == parent->_left)//如果插入的结点在父节点的左边
  47. {
  48. parent->_bf--;//因子-1
  49. }
  50. else//如果插入的结点在父节点的右边
  51. {
  52. parent->_bf++;//因子+1
  53. }
  54. //处理完成,开始检查因子
  55. if (parent->_bf == 0)//等于0,说明平衡,不需要处理
  56. {
  57. break;
  58. }
  59. //等于1或-1,说明高度变化了,那么要处理祖先结点因子
  60. else if (parent->_bf == 1 || parent->_bf == -1)
  61. {
  62. cur = parent;
  63. parent = parent->_parent;
  64. }
  65. else if (parent->_bf == 2 || parent->_bf == -2)// 如果等于2或-2,需要旋转解决
  66. {
  67. if (parent->_bf == 2 && cur->_bf == 1)//说明右边高,需左旋
  68. {
  69. RotateL(parent);
  70. }
  71. else if (parent->_bf == -2 && cur->_bf == -1)//说明左边高,需右旋
  72. {
  73. RotateR(parent);
  74. }
  75. else if (parent->_bf == 2 && cur->_bf == -1)//左子树的右子树高
  76. {
  77. RotateRL(parent);
  78. }
  79. else if (parent->_bf == -2 && cur->_bf == 1)//右子树的左子树高
  80. {
  81. RotateLR(parent);
  82. }
  83. break;
  84. }
  85. else
  86. {
  87. assert(false);//当平衡因子不为预期值,直接断言出错
  88. }
  89. }
  90. }

AVL树的旋转

在上述检查平衡因子时,当遇到parent = 2 的情况时,需要进行旋转处理。

AVL树的旋转有四种:左单旋,右单旋,左右双旋,右左双旋。

但是我们怎么判断什么情况下用什么旋转呢?

cur = 1cur = -1
parent = 2左单旋右左双旋
parent = -2左右双旋右单旋

左单旋

如下图所示:此时parent = 2 ,cur = 1

此时为:右子树的右子树新增结点导致二叉树失衡。需要进行左旋转处理。就是,右高了,左旋使两边平衡

d9e1596af92c4290aa47fbb95edc917f.png

左单旋的步骤如下:由上图可以看到,c中新增了一个结点使二叉树失衡,只看开头和结果的话,就是b当成了30结点的右子树,然后60结点当成了根结点,然后30结点当成了60结点的左子树。

那么我们将30结点定义成parent,60结点定义成subR,b结点定义成subRL。

第一步:让subRL链接parent结点

第二步:让parent结点链接subR结点

第三步:让subR结点成为_root根结点。

注意:因为每两个结点都是相互链接的,所以更新链接的时候,需要双向各自取消链接再重新链接

  1. //左单旋
  2. void RotateL(Node* parent)
  3. {
  4. //定义新指针,方便操作
  5. Node* subR = parent->_right;
  6. Node* subRL = subR->_left;
  7. Node* pp = parent->_parent;//方便更改_root的操作
  8. parent->_right = subRL;//让parent结点链接subRL
  9. subR->_left = parent;//让subR的左子树链接parent
  10. parent->_parent = subR;//由于parent的_parent由nullptr变成了subR,所以也需要重新链接
  11. if (subRL)
  12. //判断subRL是否为空,如果为空的话就不需要对subRL进行操作了,不然会出现对空指针进行解引用的问题
  13. {
  14. subRL->_parent = parent;//不为空,那么让subRL链接parent
  15. }
  16. if (_root = parent)//如果parent是整棵树的根结点
  17. {
  18. _root = subR;//subR变为根结点
  19. subR->_parent = nullptr;//subR的_parent为空
  20. }
  21. else//如果parent不是整棵树的根结点,那么将新的parent重新链接上一个结点
  22. {
  23. if (pp->_left = parent)//如果parent是上一个结点的左子树,那么新的parent也是
  24. {
  25. pp->_left = subR;
  26. }
  27. else//如果parent是上一个结点的右子树,那么新的parent也是
  28. {
  29. pp->_right = subR;
  30. }
  31. subR->_parent = pp;//更新subR的父结点
  32. }
  33. parent->_bf = subR->_bf = 0;//由于旋转后,整棵树的高度变回插入前的,那么此时parent和subR(cur)的因子都变回0
  34. }

右单旋

右单旋的逻辑与左单旋相同,这里就不多赘述了。

b389cd57f7344803bb32093c68cf113c.png

  1. //右单旋
  2. void RotateR(Node* parent)
  3. {
  4. Node* subL = parent->_left;
  5. Node* subRR = subL->_right;
  6. Node* pp = parent->_parent;
  7. //建立subL和parent之间的关系
  8. parent->_left = subRR;
  9. subL->_right = parent;
  10. //建立parent和subRR之间的关系
  11. parent->_parent = subL;
  12. if (subRR != nullptr)
  13. {
  14. subRR->_parent = parent;
  15. }
  16. //建立PP和subL之间的关系
  17. if (_root == parent)
  18. {
  19. _root = subL;
  20. subL->_parent = nullptr;
  21. }
  22. else
  23. {
  24. if (pp->_left == parent)
  25. {
  26. pp->_left = subL;
  27. }
  28. else
  29. {
  30. pp->_right = parent;
  31. }
  32. subL->_parent = pp;
  33. }
  34. //更新平衡因子
  35. subL->_bf = parent->_bf = 0;
  36. }

左右双旋

如下图,原本->插入结点->左旋->右旋

这里需要用到左右双旋是因为插入的结点不在左子树的左子树,也就是说,使二叉树失衡的是左子树的右子树新增的结点。

那么此时就需要先将新增结点的树放到最左边,变成左子树的左子树,这样就可以进行右旋了,那么怎么变呢,我们只需要将30结点和60结点当成我们上面所学的左单旋例子即可

也就是说,用30结点子树传入进行左单旋(图的右下角),然后在让90结点的子树进行右旋(图的左下角)

0ef7e86b148a465691fec40768af11e6.png

 我们看原本和结果:b成了30结点右子树,c成了90结点左子树,30和90分别成了60结点的左右子树,60结点成为了根结点。

那么我们左旋传入的结点就是30结点,旋转完成后,再将 90 结点传入进行右旋。

平衡因子的更新:

在双旋之后,我们可以发现,原本60结点的左子树,变成了30结点的右子树,60结点的右子树,变成了90结点的左子树,那么我们只需要根据60结点的平衡因子,就能知道到底是30结点的平衡因子改变了还是90结点的平衡因子改变了。当然,subR/subL结点的平衡因子由图所示都更新成0

  1. void RotateLR(Node* parent)//左右双旋
  2. {
  3. Node* subL = parent->_left;
  4. Node* subLR = subL->_right;
  5. int bf = subLR->_bf;
  6. RotateL(parent->_left);
  7. RotateR(parent);
  8. if (bf == 0)
  9. {
  10. //subLR自己就是新增
  11. subLR->_bf = 0;
  12. subL->_bf = 0;
  13. parent->_bf = 0;
  14. }
  15. else if (bf == -1)
  16. {
  17. //subLR的左子树新增
  18. subLR->_bf = 0;
  19. subL->_bf = 0;
  20. parent->_bf = 1;
  21. }
  22. else if(bf == 1)
  23. {
  24. //subLR的右子树新增
  25. subLR->_bf = 0;
  26. subL->_bf = -1;
  27. parent->_bf = 0;
  28. }
  29. else
  30. {
  31. assert(false);
  32. }
  33. }

右左双旋

也是和左右双旋基本相同,先记录需要记录的值,然后先右旋,再左旋,最后更新平衡因子即可。

  1. void RotateRL(Node* parent)//右左双旋
  2. {
  3. Node* subR = parent->_right;
  4. Node* subRL = subR->_left;
  5. int bf = subRL->_bf;
  6. RotateR(parent->_right);
  7. RotateL(parent);
  8. if (bf == 0)
  9. {
  10. //subRL自己就是新增
  11. parent->_bf = subR->_bf = subRL->_bf = 0;
  12. }
  13. else if (bf == -1)
  14. {
  15. //subRL的左子树新增
  16. parent->_bf = 0;
  17. subRL->_bf = 0;
  18. subR->_bf = 1;
  19. }
  20. else if (bf == 1)
  21. {
  22. //subRL的右子树新增
  23. parent->_bf = -1;
  24. subRL->_bf = 0;
  25. subR->_bf = 0;
  26. }
  27. else
  28. {
  29. assert(false);
  30. }
  31. }

AVL树的验证

验证为二叉搜索树

想要验证为二叉搜索树,即采用中序遍历,如果为升序,那么就是二叉搜索树

还是一样,需要在内部类调用私有成员。

  1. //中序遍历副函数
  2. void Inorder()
  3. {
  4. _Inorder(_root);
  5. }
  6. //中序遍历主函数
  7. void _Inorder(Node* root)
  8. {
  9. if (root == nullptr)
  10. return;
  11. _Inorder(root->_left);
  12. cout << root->_kv.first << " ";
  13. _Inorder(root->_right);
  14. }

验证为AVL树

1.每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)

2.节点的平衡因子是否计算正确。

那么,步骤也就变成了:采用后序遍历

092467846f5442f0b59a15caec2f1cd7.png

1.从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)

2.先判断左子树是否是平衡二叉树。

3.再判断右子树是否是平衡二叉树。

4.若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

  1. //判断是否为AVL树
  2. bool IsAVLTree()
  3. {
  4. int hight = 0; //输出型参数
  5. return _IsBalanced(_root, hight);
  6. }
  7. //检测二叉树是否平衡
  8. bool _IsBalanced(Node* root, int& hight)
  9. {
  10. if (root == nullptr) //空树是平衡二叉树
  11. {
  12. hight = 0; //空树的高度为0
  13. return true;
  14. }
  15. //先判断左子树
  16. int leftHight = 0;
  17. if (_IsBalanced(root->_left, leftHight) == false)
  18. return false;
  19. //再判断右子树
  20. int rightHight = 0;
  21. if (_IsBalanced(root->_right, rightHight) == false)
  22. return false;
  23. //检查该结点的平衡因子
  24. if (rightHight - leftHight != root->_bf)
  25. {
  26. cout << "平衡因子设置异常:" << root->_kv.first << endl;
  27. }
  28. //把左右子树的高度中的较大值+1作为当前树的高度返回给上一层
  29. hight = max(leftHight, rightHight) + 1;
  30. return abs(rightHight - leftHight) < 2; //平衡二叉树的条件
  31. }

AVL树的性能

AVL树是一颗绝对平衡的二叉树,其要求每个结点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(logN)。但是要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/582665
推荐阅读
相关标签
  

闽ICP备14008679号