• AVL部分功能实现和了解


    我先前写过一篇二叉搜索树的博客,在那篇博客中我介绍了二叉搜索树的k结构和kv结构实现法,当时也留了个问题,就是普通的二叉搜索树是有缺陷的,可能会退化为链表,从而使得搜索效率降低为O(n),解决方法是对二叉搜索树调平衡,下面实现的AVL树以及后面提及红黑树的实现都将二叉搜索树调平衡了。

    一 节点类

      大家可以看到我们在节点类中增加了一个变量_v1,_v1存的是平衡因子,这个平衡因子是用右子树的高度减去左子树高度得到的, 这个树还是一个三叉链(除了_left和_right指针,还有_parent指针指向父节点)的结构,三叉链结构对于后续平衡因子的更新有十分大的作用。

    1. template<class T,class k> kv结构二叉树
    2. struct AVLTreeNode
    3. {
    4. AVLTreeNode(const pair& p1)
    5. :_p1(p1)
    6. ,_left(nullptr)
    7. ,_right(nullptr)
    8. ,_parent(nullptr)
    9. , _v1(0)
    10. {
    11. ;
    12. }
    13. pair _p1; 用一个pair存放key和val,方便一起把key和val返回,使用要包含头文件
    14. AVLTreeNode* _left;
    15. AVLTreeNode* _right;
    16. AVLTreeNode* _parent;
    17. int _v1;//存放平衡因子
    18. };

     二 AVL树

    这个类是通过管理_root来达到管理,描述整棵树,其实用一个类也可以起作用,在节点类多加一个_root指针不就OK了,至于为什么要将管理树的管理节点的分成两个类,感觉是一种玄妙的设计思想,每个节点都加上_root指针,实际上每个节点却用不上_root指针,这就使得每个节点都会存一个无用信息,有些场景旋转是会改变根的,你想想每个节点都存_root,维护起来太麻烦了,为了树的封装性,用类封装_root,使其不被外界随意访问也是非常有意义的,而insert,find函数都要获取根,所以注定了树的成员函数和_root指针一起封装。

       AVL本质还是个二叉搜索树,只是它规定左右子树的高度差必须小与等于1,这时候肯定有人想问,为什么不能是零呢,这样不就绝对完美平衡吗,因为太难了,你想想高度差为零只有在满二叉树才会出现,而现实中几乎很难遇到这样的树。接下来我们一步步实现AVL树,首先实现insert功能。

    1 插入新节点

    由于AVL树本质就是二叉搜索树,所以在插入节点时还是比大小来判断是插入在根节点的左边还是右边插入。如果之前我们就实现了一下二叉搜索树,找插入位置插入节点的代码还是很简单的。麻烦的是更新平衡因子。要分多种情况,我们将插入节点称为newnode,其双亲节点为parent,

    1. template<class T, class k>
    2. class AVLTree
    3. {
    4. public:
    5. typedef AVLTreeNode Node;
    6. bool Insert(const pair& p1)
    7. {
    8. if (_root == nullptr)
    9. {
    10. _root = new Node(p1);
    11. return true;
    12. }
    13. Node* cur = _root;
    14. Node* parent = _root;
    15. while (cur)//按照搜索二叉树规则插入节点
    16. {
    17. if (cur->_p1.first
    18. {
    19. parent = cur;
    20. cur = cur->_right;
    21. }
    22. else if (cur->_p1.first > p1.first)
    23. {
    24. parent = cur;
    25. cur = cur->_left;
    26. }
    27. else
    28. {
    29. return false;
    30. }
    31. }
    32. 当cur等于nullptr时,
    33. 插入节点
    34. cur = new Node(p1);
    35. if (parent->_p1.first>p1.first)//小于parent的key,插入到左边
    36. {
    37. parent->_left = cur;
    38. }
    39. else //大于parent的key,插入到右边
    40. {
    41. parent->_right = cur;
    42. }
    43. cur->_parent = parent;
    44. return true;
    45. }
    46. Node* _root=nullptr; 根节点指针,要初始化为nullptr
    47. };

    2 平衡因子的更新

    下面代码都在while循环内,不好分离开,只能将解析放在里面了。

    1. 更新平衡因子
    2. while (parent)
    3. {
    4. if (parent->_left == cur) 插入节点在parent的左侧,平衡因子--
    5. {
    6. parent->_v1--;
    7. }
    8. else
    9. {
    10. parent->_v1++; 插入节点在parent的左侧,平衡因子++
    11. }
    12. 重点来了,当更新后parent的平衡因子为-1/1,这说明先前为0,不可能是2,-2
    13. 因为后面会对这两种情况做处理,
    14. 先前_v1==0,说明两边高度一致,现在变为-1,1,说明parent所在树高度变了,要更新parent的双亲节点的平衡因子。
    15. if (parent->_v1 == -1 || parent->_v1 == 1)
    16. {
    17. cur = parent;
    18. parent=parent->_parent;
    19. }
    20. else if (parent->_v1 == 0)
    21. 先前是-1/1,是一边高的,现在变成0,变成两边一样高
    22. parent所在子树最大高度不变,无需更新
    23. {
    24. break;
    25. }
    26. else if (parent->_v1 == -2||parent->_v1==2)//旋转
    27. {
    28. if (parent->_v1 == 2 && cur->_v1 == 1)
    29. RotateL(parent);
    30. else if (parent->_v1 == -2 && cur->_v1 == -1)
    31. RotateR(parent);
    32. else if (parent->_v1 == 2 && cur->_v1 == -1)
    33. RotateRL(parent);
    34. else if (parent->_v1 == -2 && cur->_v1 == 1)
    35. RotateLR(parent);
    36. else
    37. assert(false);
    38. return true;
    39. }
    40. else
    41. {
    42. assert(false);
    43. }
    44. }

    旋转大家只要先知道平衡因子为-2/2时要用旋转解决就好,原理会在下面旋转示意图中解析。

    3 旋转示意图

    为了更好理解旋转,我画了几个图来说明不同的旋转。

    1 左单旋

    0e30f5ea1db242e7980cadbe822a5119.png

      上图是所有左单旋的场景,我们假设节点5是要进行左旋转的点,插入前平衡因子就必须是1(那我们就假设节点5的左树高n,右树高n+1)插入节点后变为2,那节点就是在5的右树插入的,而且也必须是在6节点的右树,如果是在6节点的左树,这种在双旋会再提及。那6节点,7节点先前的平衡因子很明显也就只能是0,只有这样平衡因子的更新才会波及到节点5(-1会停止,1则是在节点6进行旋转,不符合假设在节点5进行旋转),那6节点的左树,右树高都是n,7节点的左树,右树高都是n-1。

    对5进行左单旋: 如果节点6的左树不为空,就把节点6的左树给节点5的_right指针,节点5整颗树做节点的左树。(只有这样能保证二叉搜索树中左树小于根,右子树大于根的特性)

    1. void RotateL(Node* parent)//左旋转
    2. {
    3. Node* cur = parent->_right;
    4. Node* Curleft = cur->_left;
    5. //旋转
    6. Node* pparent = parent->_parent;
    7. cur->_left = parent;
    8. parent->_right = Curleft;
    9. parent->_parent = cur;
    10. cur->_parent = pparent;
    11. if (Curleft) cur的左子树不为空时
    12. {
    13. Curleft->_parent = parent;
    14. }
    15. 平衡因子修改
    16. parent->_v1 = 0;
    17. cur->_v1 = 0;
    18. if (parent == _root)//改变根
    19. {
    20. _root = cur;
    21. }
    22. else//和上一层链接
    23. {
    24. if (pparent->_left == parent)
    25. pparent->_left = cur;
    26. else
    27. pparent->_right = cur;
    28. }
    29. }

    旋转结果,代码要画图结合旋转过程才好理解,毕竟涉及到多个节点的链接。

    71bfb2e0816c45e0b3c55279185d5d65.png

    此时我们可以肯定的把节点5,6的平衡因子改为0,因为我们把6的左树(高为h)给了节点5,那节点5左右树高度相同,因子为零没毛病,而6的因子在更新中变成了1,但是由于左树高度变成n+1了,也为零,而7的平衡因子在插入的时候就会被更新成1或者-1,不用处理。(最后提一下就是当树高n=0时,节点7就是新插入节点)

    2 右单旋

    92a92f868ab04df99bd6d462ac5aecbb.png

    再来看看右单旋,还是假设节点5为旋转点,5的平衡因子必是-1,那节点4和节点3的平衡因子都是0,道理如上。此时插入节点必须是4的左树,在右树是双旋的另一个场景。还有就是插入节点可以是3的左树,也可以是右树,甚至可能就是节点3本身,但是都可以用右单旋搞定。

    对5进行右单旋: 如果节点4的右树不为空,就把节点4的右树给节点5的_left指针,节点5整颗树做4节点的右树。如下图,还是一颗二叉搜索树。

    5c5466f6601843db9f47b4ef53e5d640.png

    1. void RotateR(Node* parent)
    2. {
    3. Node* cur = parent->_left;
    4. Node* Curright = cur->_right;
    5. //旋转
    6. Node* pparent = parent->_parent;//记录该树parent节点的父节点
    7. cur->_right = parent;
    8. parent->_left = Curright;
    9. parent->_parent = cur;
    10. cur->_parent = pparent;
    11. if (Curright)//cur的右子树不为空时
    12. {
    13. Curright->_parent=parent;
    14. }
    15. //平衡因子修改
    16. parent->_v1= 0;
    17. cur->_v1 = 0;
    18. if (parent == _root)//改变根
    19. {
    20. _root = cur;
    21. }
    22. else//和上一层链接
    23. {
    24. if (pparent->_left == parent)
    25. pparent->_left = cur;
    26. else
    27. pparent->_right = cur;
    28. }
    29. }
    3 右左双旋

    之前说左单旋的插入节点必须是节点7(当时这个节点位置数据是6,我们要对应位置,而不是对应值)这个位置的右树才发生右单旋,如果是7的左树则是双旋,我们举个例子,当n=0时,节点6就是插入节点,这个时候如果还是用右单旋,大家可以试试结果会是平衡的吗?

     4d6b16586c624ac2a2e35c1379ceb8c3.png

    结果如下,我都不用画平衡因子来验证了。这说明这种情况无法用单旋保持平衡。

    f9d7ebf42cf041af9963543de7cf2505.png

    好,我们再来看一般情况。

    520f28e145ac431f8a55f84ca81d230f.png

    还是假设节点5是是更新后平衡因子为2的节点,那一开始必然是1,那就设左树高为n,右树高为n+1,而节点6,7因子均为0,那7的右树就为n,6的左右树均为n-1(也可能一边高,但我们为了简化,使得在六的左树和右树插入一定导致节点6这颗树高度增大)。

    右左双旋:先对节点7做右旋,结果如下,下一步是对5进行左旋,那就不会影响节点7的平衡因子了,此时是0。下图为情况(1):在6的右树插入节点

    可是如果新插入节点是6节点的左树,此时节点7的平衡因子就应该是1,也就是说7的平衡因子是要分情况讨论的。下图为情况(2):在6的左树插入节点

    再对节点5做左旋,

    下图为情况(1)的左旋:在6的右树插入节点

    下图为情况(2)的左旋图:在6的左树插入节点

    这时候有两个有意思的点,双旋就是把节点6的右树给节点7,然后把左树给了节点5,节点6做了根,而bf=1或者为-1,就会对节点5和节点7的平衡因子有影响,我们惊奇的发现,最后平衡因子结果是可以知道的。(这里是需要画图慢慢理解的,这里就已经是比较复杂的,相信自己一定可以弄明白的)

    1. void RotateRL(Node* parent)
    2. {
    3. Node* cur = parent->_right;
    4. Node* curleft = cur->_left;
    5. int bf=cur->_left->_v1; 保存图中节点6的平衡因子
    6. RotateR(cur);
    7. RotateL(parent);
    8. if (bf == 0) 等于0说明节点6本身是新插入节点,此时5,67节点因子归零即可
    9. {
    10. cur->_v1 = 0;
    11. parent->_v1 = 0;
    12. Curright->_v1 = 0; 在左单旋和右单旋中我们都粗暴地对旋转节点和它的子节点的
    13. 平衡因子归零,为什么出来还要再写一遍呢?
    14. 这是为了解耦,防止单旋代码影响了双旋代码
    15. }
    16. else if (bf == 1)
    17. {
    18. cur->_v1 = 0;
    19. curleft->_v1 = 0;
    20. parent->_v1 = -1;
    21. }
    22. else if (bf == -1)
    23. {
    24. cur->_v1 = 1;
    25. curleft->_v1 = 0;
    26. parent->_v1 = 0;
    27. }
    28. else
    29. ;
    30. }
    4 左右双旋

    从前面介绍的右左双旋我们知道双旋实质上是由单旋构成的,也就是说我们可以复用单旋的代码。

    先前在介绍右单旋的时候提到,如果插入节点是节点4的右树,那就要双旋,如下例子。

    此时可以用这个图试试用右单旋,结果就是这样。

    好,我们再来看左右双旋的一般情况。

    再次强调,当n=0时,节点4就是新插入节点。

    我就直接开始介绍左右双旋如何旋吧,先以节点3作为旋转点做左单旋;

    然后就是对节点五进行右单旋。结果如下

    节点3的左树和节点5的右树就是原来节点4的子树如果我们是在节点4的左子树插入一个节点,此时节点3的平衡因子就应该是0,节点5就应该是1。相反,如果插入在节点4的右子树,那5的平衡因子就应该是0, 3的平衡因子就应该是1。代码如下

    1. void RotateLR(Node* parent)
    2. {
    3. Node* cur = parent->_left;
    4. Node* Curright = cur->_right;
    5. int bf = cur->_right->_v1;
    6. RotateL(cur);
    7. RotateR(parent);
    8. if (bf == 0)//解耦
    9. {
    10. cur->_v1 = 0;
    11. parent->_v1 = 0;
    12. Curright->_v1 = 0;
    13. }
    14. else if (bf == 1)
    15. {
    16. parent->_v1 = 0;
    17. cur->_v1 = -1;
    18. Curright -> _v1 = 0;
    19. }
    20. else if (bf == -1)
    21. {
    22. cur->_v1 = 0;
    23. parent->_v1 = 1;
    24. Curright -> _v1 = 0;
    25. }
    26. }

    三 AVL树检验

    这一步很重要,很多时候我们运行成功了,不能说明代码没问题,随便弄一个测试用例也不足以说明代码没问题,最好的办法就是用大量的随机数产生的序列来测试,大量,越多越好,亲身经历,有时候十几个随机数没问题,二十几个就出问题了,这时候真的很坑,这个时候我是打印了一串出错的序列,然后一步步画图调试,才找到问题。接下来就看看如何检验的吧。

    AVL树首先是二叉搜索树,而且每个节点的左右子树高度差小于1,所以我们只要检查这两个特性就好了。

    二叉搜索树我没检验,因为我直接copy我上篇博客写的二叉搜索树代码,之前没问题,应该可以直接用,嘻嘻。

    1. int Height(Node*root)
    2. {
    3. if (root == nullptr)
    4. return 0;
    5. int left = Height(root->_left);
    6. int right = Height(root->_right);
    7. return right > left ? right + 1 : left + 1;
    8. }
    9. bool isBlanceTree()
    10. {
    11. return isBlanceTree(_root);
    12. }
    13. 前序遍历,反复求高度
    14. bool isBlanceTree(Node* root)
    15. {
    16. if (root == nullptr)//根为空,返回true
    17. return true;
    18. int left = Height(root->_left);//去计算左子树高度
    19. int right = Height(root->_right);//去计算又子树高度
    20. int bf = (right - left);//这是我们算的平衡因子
    21. if (bf != root->_v1 || bf > 1 || bf < -1)//如果和_v1不同或者,_v1的值不合规,就不是AVL
    22. return false;
    23. 去看看根的左子树和右子树的根是否合理
    24. return isBlanceTree(root->_left) && isBlanceTree(root->_right);
    25. }

    前序遍历求每个节点的平衡因子的效率是偏低的,因为我们做了大量的重复计算。比如一开始计算的是_root的平衡因子是否符合要求,那就要算每个节点的左右子树高,而_root-left和_root-right的平衡因子计算又要再求这些节点高度。

    所以我们可以优化一下。

    1. 优化,合并为后序
    2. int isBlanceTree3(Node* root,bool& a)用参数a记录子树是否合格
    3. {
    4. if (root == nullptr)
    5. {
    6. a = true;
    7. return 0;
    8. }
    9. //去子树确认平衡因子是否正确
    10. int left = isBlanceTree3(root->_left,a);
    11. bool lefta = a;
    12. int right = isBlanceTree3(root->_right,a);
    13. bool righta = a;
    14. //计算根的平衡因子
    15. int bf = (right- left);
    16. //计算root子树最大高度
    17. int Max = left > right ? left: right;
    18. //确认根的平衡因子是否和bf相同,以及bf是否合法
    19. if (bf != root->_v1 || bf > 1 || bf < -1)
    20. {
    21. a = false;
    22. return Max;
    23. }
    24. //左右子树合格,根合格,返回树高度加1
    25. if (lefta && righta)
    26. {
    27. return Max + 1;
    28. }
    29. else
    30. return 0;
    31. }

    当我们遇到难的知识点,说明我们在爬坡,过去了就成长了。

  • 相关阅读:
    Scala的函数至简原则
    第三章、组织代码
    「九章云极DataCanvas」完成C+轮融资, 用云中云战略引领数据智能基础软件升级
    Java基础36 super关键字
    代理模式(静态代理、JDK代理、CGLIB代理)
    基于OFDM的通信系统模拟实现
    动态内存管理(C语言)
    数据链路层协议 ——— 以太网协议
    【Java基础系列】循环与迭代
    yolox小计
  • 原文地址:https://blog.csdn.net/m0_62792369/article/details/132797426