• 【C++】 AVL树


    目录

    1.AVL数的概念:

    2.AVL树的结构

     2.1结点结构

    2.2基本结构 

    3.插入函数(Insert) 

    3.AVL树的旋转

    3.1左单旋

    3.2右单旋 

     3.3左右双旋

    3.4右左双旋 

    3.5测试插入代码

    3.6检查是否为平衡树 


    代码在码云仓库:

    https://gitee.com/j-jun-jie/c---advanced.giticon-default.png?t=M85Bhttps://gitee.com/j-jun-jie/c---advanced.git

    1.AVL数的概念:

    计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。

    之所以会用到AVL树就是因为当我们使用搜索二叉树时会出现当插入的元素有序或者接近有序二叉树会退化成单支树的情况,所以对二叉树的底层做了些改进,用平衡树实现。

    平衡树就是为了防止搜索二叉树退化成单支树,AVL树就是平衡二叉树。

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

    1. 它的左右子树都是AVL树
    2. 任何一颗左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

     至于这个-1,0,1是如何算的,各位心中有点小疑惑,我们称它叫平衡因子

    平衡因子=右子树高度-左子树高度。比如9,右子树高度是2,左子树高度是1,平衡因子是1。不出意外左子树的平衡因子一定是非负数。

     如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在,搜索时间复杂度O()。

    2.AVL树的结构

     2.1结点结构

    这里我们实现的AVL树为KV模型,自然节点的模板参数有两个,并且节点定义为三叉连结构(左孩子,右孩子,父亲)在二叉链表的基础上加了一个指向父结点的指针域,使得即便于查找孩子结点,又便于查找父结点。

    上面我们说还有一个平衡因子,但是这个变量并不是节点结构的必备元素,但是创建这个元素更方便一些。

    1. //节点类
    2. template<class K, class V>
    3. struct AVLTreeNode
    4. {
    5. //三叉连结构
    6. AVLTreeNode* _left;//左孩子
    7. AVLTreeNode* _right;//右孩子
    8. AVLTreeNode* _parent;//父亲
    9. //节点值存储的键值对
    10. pair _kv;
    11. //平衡因子_bf
    12. int _bf;//右子树 - 左子树的高度差
    13. //构造函数进行初始化
    14. AVLTreeNode(const pair& kv)
    15. :_kv(kv)
    16. , _left(nullptr)
    17. , _right(nullptr)
    18. ,_parent(nullptr)
    19. , _bf(0)
    20. {}
    21. };

    2.2基本结构 

    1. //AVL树的类
    2. template<class K, class V>
    3. class AVLTree
    4. {
    5. typedef AVLTreeNode Node;
    6. public:
    7. //……
    8. private:
    9. Node* _root;
    10. };

     我们先不管,构造,析构,拷贝构造函数,一会再来完善它。

    3.插入函数(Insert) 

    在搜索二叉树那,我们比较的是cur->key,但是这里我们引用了键值对,所以比较的时候有点不同。

    cur->_kv.first和kv.first,这里的kv就相当于上面的key但是它存储的有key(first),value(second)所以我们比较它的first.

    插入时分4大步:

    • 根节点为空,直接插入新的根节点。
    • while(cur)找合适的插入位置

    所以这里分好几种情况;

    1. _kv.first>kv.first,把它连接到父节点的左边。
    2. _kv.first
    3. 插入值重复,返回false。
    • 链接·插入的结点:
    1. 插入到那边就链接到那边。
    2. 因为是三叉链,所以要把cur的_parent链接parent。
    • 更新平衡因子,看是否二叉树平衡

    在插入数据后,更新平衡因子。

    1. bf=0,不用变,直接break跳出循环
    2. |bf|=1,利用三叉链的parent往上走,更新平衡因子
    3. |bf|=2 旋转,使二叉树平衡(左右单旋,右左,左右旋四种)
    4. |bf|大于2,小于-2,断言到3,说明上面bf=2是未处理,返回处理。
    所以简洁模板诞生:
    1. bool Insert(const pair& kv)
    2. {
    3. //结点为空,kv变成新结点
    4. if (_root == nullptr)
    5. {
    6. //如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf
    7. _root = new Node(kv);
    8. _root->_bf = 0;
    9. return true;
    10. }
    11. //2、寻找插入的合适位置
    12. //非递归时起链接作用的两大天王
    13. Node* parent = nullptr;
    14. Node* cur = _root;
    15. while (cur)
    16. {
    17. if (cur->_kv.first < kv.first) //kv有两个参数,第一个是key,所以比较first
    18. {
    19. parent = cur;
    20. cur = cur->_right;
    21. }
    22. else if (cur->_kv.first > kv.first)
    23. {
    24. parent = cur;
    25. cur = cur->_left;
    26. }
    27. else
    28. {
    29. return false;
    30. }
    31. }
    32. //链接新节点
    33. cur = new Node(kv);
    34. if (parent->_kv.first < kv.first)
    35. {
    36. //插入的值 > 父亲的值,链接在父亲的右边
    37. parent->_right = cur;
    38. }
    39. else
    40. {
    41. //插入的值 < 父亲的值,链接在父亲的左边
    42. parent->_left = cur;
    43. }
    44. cur->_parent = parent;
    45. //更新平衡因子

    插入完数据就要平衡二叉树了,因为插入的位置不同,平衡因子就改变了:

     更新平衡因子的规则:

    • 1.新增在右,parent->_bf++,新增在左,parent->_bf--.
    • 2.更新后,如果parent->_bf变成-1/1了说明原来是0,高度升高,需要继续往上更新。
    • 3.更新后,如果parent->_bf变成0了说明原来是-1/1,高度没变,不需要继续往上更新。
    • 4.更新后,如果parent->_bf变成0了说明原来是2/-2,就说明原来就是1/-1又高一层,平衡被破坏了。
    1. while (parent) //parent为空跳出,root无父节点
    2. {
    3. if (cur == parent->_left)
    4. {
    5. parent->_bf--;
    6. }
    7. else
    8. {
    9. parent->_bf++;
    10. }
    11. if (parent->_bf == 0)
    12. {
    13. break; //不需要更改,直接跳出循环
    14. }
    15. else if (abs(parent->_bf) == 1)
    16. {
    17. parent = parent->_parent;
    18. cur = cur->_parent; //往上走,更改平衡因子
    19. }
    20. else if (abs(parent->_bf) == 2)
    21. {
    22. //平衡出错,旋转,有以下四种旋转
    23. if (parent->_bf == 2 && cur->_bf == 1)
    24. {
    25. RotateL(parent);//右边高,左单旋
    26. }
    27. else if (parent->_bf == -2 && cur->_bf == -1)
    28. {
    29. RotateR(parent);//左边高,右单旋
    30. }
    31. else if (parent->_bf == -2 && cur->_bf == 1)
    32. {
    33. RotateLR(parent);//左右双旋
    34. }
    35. else if (parent->_bf == 2 && cur->_bf == -1)
    36. {
    37. RotateRL(parent);//右左双旋
    38. }
    39. break;
    40. }
    41. else //_bf大于2,小于-2,理论来说不会走到这一步上一步就应该处理
    42. {
    43. assert(false); //直接报错,让上一步处理
    44. }
    45. }

    平衡破坏后如何旋转:

    1.旋转后的原则:旋转成平衡树,保持还是搜索树

    3.AVL树的旋转

    旋转的四种分类:

    • 1.左单旋
    • 2.右单旋
    • 3.左右双旋
    • 4.右左双旋

    3.1左单旋

    新节点插入较高右子树的右边

    抽象图解;

    具体图像;

    h=0;

    h=1:

     h=2就不在列举了,如果bf=2,例如30,就把它按在60的左子树那里。这样就解决了平衡因子大于2的问题了。

    这里的本质都是引发了parent->_bf==2,其他都是1,所以我们把2的这个结点往下按。

     

    但是在这里也分两种情况;

    • 1.parent是根:

    直接让SubR变成根,但是SubR的父节点要指向空。

    • 2.parent是子树的根 ,但不是总根。

    所以旋转后要让SubR指向上一个结点。但是要先判断一下parent在左边还是右边。

    整体思路:

    • parent的右孩子担任根节点,但是判断SubL是否有左孩子
    • 把SubRL链接到parent上,把parent链接到SubR的左子树上。
    • 判断原来的parent是根,还是子树的根,先用ppNode保存parnet->parent,如果是根ppNode就是nullptr.
    • parent是根,SubR的parent制空。
    • parent是子节点,判断是左节点还是右节点,链接它的parent和ppNode.
    • SubR,parent平衡因子改为0.
    1. //左单旋
    2. void RotateL(Node* parent)
    3. {
    4. Node* SubR = parent->_right;
    5. Node* SubRL = SubR->_left;
    6. Node* ppNode = parent->_parent; //如果parent不是根,记录它的上一层节点
    7. parent->_right = SubRL;
    8. if (SubRL) //SubR的左子树存在,链接到parent的右子树上
    9. {
    10. SubRL->_parent=parent;
    11. }
    12. SubR->_left = parent; //父节点链接到SubR的左子树上
    13. parent->_parent = SubR;
    14. if (parent == _root) //parent是根节点
    15. {
    16. _root=SubR ;
    17. _root->_parent = nullptr;
    18. }
    19. else //parent是子节点
    20. {
    21. if (ppNode->_left == parent) //parent在父节点的左节点处
    22. {
    23. ppNode->_left = SubR;
    24. }
    25. else
    26. {
    27. ppNode->_right = SubR;
    28. }
    29. SubR->_parent = ppNode; //把SubR的父节点链接到它的父节点上面
    30. }
    31. SubR->_bf = parent->_bf = 0; //平衡因子改为0
    32. }

    3.2右单旋 

     那个词咋说的?如出一辙?重蹈覆辙?哈哈!

     不讲了直接上代码:

    1. //右单旋
    2. void RotateR(Node* parent)
    3. {
    4. Node* SubR = parent->_left;
    5. Node* SubRL = SubR->_right;
    6. if (SubRL) //SubR的右子树存在,链接到parent的右子树上
    7. {
    8. SubRL->_parent = parent;
    9. }
    10. Node* ppNode = parent->_parent; //如果parent不是根,记录它的上一层节点
    11. SubR->_right = parent; //父节点链接到SubR的左子树上
    12. parent->_parent = SubR;
    13. if (parent == _root) //parent是根节点
    14. {
    15. _root = SubR;
    16. SubR->_parent = nullptr;
    17. }
    18. else //parent是子节点
    19. {
    20. if (ppNode->_left == parent) //parent在父节点的左节点处
    21. {
    22. ppNode->_left = SubR;
    23. }
    24. else
    25. {
    26. ppNode->_right = SubR;
    27. }
    28. SubR->_parent = ppNode; //把SubR的父节点链接到它的父节点上面
    29. }
    30. SubR->_bf = parent->_bf = 0; //平衡因子改为0
    31. }

     3.3左右双旋

    看一下这个图;

    此时60的平衡因子改变了。30的改变成1(要包含60),所以跟90的平衡因子变成2了,所以要旋转,先来个左旋,在来个右旋,平衡树就满足了。

     在b,c处插入,平衡因子不同,双旋最棘手的就是平衡因子的改变。 

    还有一种情况:

     左右双旋就是先来个左旋,再来个右旋,但是这个平衡因子可能不太一样,就比如上面的三种情况,这三个结点的平衡因子都不一样。既然有左旋,有右旋,那我们可以直接复用上面的代码:

    但是平衡因子如何改?

    这三种情况,结点60处的平衡因子各不一样,正好3个数都取到了。让60做参考

    1. void RotateLR(Node* parent)
    2. {
    3. Node* SubL = parent->_left;
    4. Node* SubLR = SubL->_right;
    5. int bf = SubLR->_bf; //先记录下bf
    6. RotateL(SubL); //左旋
    7. RotateR(parent); //右旋
    8. //按节点60的平衡因子分三种情况
    9. if (bf == 0)
    10. {
    11. parent->_bf = 0;
    12. SubL->_bf = 0;
    13. SubLR->_bf = 0;
    14. }
    15. else if (bf == 1)
    16. {
    17. parent->_bf = 0;
    18. SubL->_bf = -1;
    19. SubLR->_bf = 0;
    20. }
    21. else if (bf == -1)
    22. {
    23. parent->_bf = 1;
    24. SubL->_bf = 0;
    25. SubLR->_bf = 0;
    26. }
    27. else
    28. {
    29. assert(false);//此时说明旋转前就有问题,检查
    30. }
    31. }

    3.4右左双旋 

    这个思路跟上面的左右双旋一样,先来一个右单旋,再来一个左单旋

    1. //4、右左双旋
    2. void RotateRL(Node* parent)
    3. {
    4. Node* SubR = parent->_right;
    5. Node* SubRL = SubR->_left;
    6. int bf = SubRL->_bf;//提前记录subLR的平衡因子
    7. //1、以SubL为根传入左单旋
    8. RotateR(SubR);
    9. //2、以parent为根传入右单旋
    10. RotateL(parent);
    11. //3、重新更新平衡因子
    12. if (bf == 0)
    13. {
    14. parent->_bf = 0;
    15. SubR->_bf = 0;
    16. SubRL->_bf = 0;
    17. }
    18. else if (bf == 1)
    19. {
    20. parent->_bf = -1;
    21. SubR->_bf = 0;
    22. SubRL->_bf = 0;
    23. }
    24. else if (bf == -1)
    25. {
    26. parent->_bf = 0;
    27. SubR->_bf = 1;
    28. SubRL->_bf = 0;
    29. }
    30. else
    31. {
    32. assert(false);//此时说明旋转前就有问题,检查
    33. }
    34. }

    3.5测试插入代码

    1.我们用中序遍历:

    1. //中序遍历
    2. void InOrder()
    3. {
    4. _InOrder(_root);
    5. cout << endl;
    6. }
    7. private:
    8. //中序遍历的子树
    9. void _InOrder(Node* root)
    10. {
    11. if (root == nullptr)
    12. return;
    13. _InOrder(root->_left);
    14. cout << root->_kv.first<<":" << root->_kv.second << " ";
    15. cout << endl;
    16. _InOrder(root->_right);
    17. }
    1. void test1_AVLTree()
    2. {
    3. int a[] = { 16,3,7,11,9,26,18,14,15};
    4. AVLTree<int, int> t;
    5. for (auto e: a)
    6. {
    7. t.Insert(make_pair(e, e));
    8. }
    9. t.InOrder();
    10. }

     由于插入时旋转以及各种情况实在太多,很难避免出现一些错误,希望各位大佬指正一下。

    3.6检查是否为平衡树 

    上面说如果平衡因子的数是-1,0,1就证明是平衡树,但是我们检查的时候就不能用平衡因子了,因为平衡因子的数就是我们自己实现的,再用平衡因子检查是否是二叉树那不是监守自盗吗?

    所以我们要找一个无关量检查-----高度

    1. //求二叉树的高度
    2. int Height(Node* root)
    3. {
    4. if (root == nullptr)
    5. {
    6. return 0;
    7. }
    8. int LeftH = Height(root->_left);
    9. int RightH = Height(root->_right);
    10. return max(LeftH, RightH) + 1; //根的高度是子树+1
    11. }

    但是光有这一个代码很显然不行,我们在写一个判断平衡函数,这个平衡函数不能只判断一个节点,它的孩子节点是否平衡也要判断。

    1. //判断平衡
    2. bool Isbalence()
    3. {
    4. return _Isbalence(_root);
    5. }
    6. private:
    7. //判断是否平衡
    8. bool _Isbalence(Node* root)
    9. {
    10. if (root == nullptr)
    11. return true;
    12. int LeftH = Height(root->_left);
    13. int RightH = Height(root->_right);
    14. int deff = RightH - LeftH; //平衡因子的绝对值
    15. //如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树
    16. if ((abs(deff) > 1))
    17. {
    18. cout << root->_kv.first << "节点平衡因子异常" << endl;
    19. return false;
    20. }
    21. if (deff != root->_bf)
    22. {
    23. cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;
    24. return false;
    25. }
    26. return _Isbalence(root->_left) && _Isbalence(root->_right);
    27. //还要判断他的左子树,右子树
    28. }

    之所以把private也复制进去就是想说,这两个函数一个在公共区,一个在私有区。

    但是虽然能打印出平衡因子了,但是会不会打印出来的是错误的?

    我们举一个双选的实例来判断一下。

    如果我们不更新平衡因子:

     6的平衡因子是0,但是他的右边是空,左边还有数值,所以6的平衡因子应该是-1.

    但是更新平衡因子后就没问题了;

    4的平衡因子为1也是正确的。 

    4.AVL树的查找:

    查找函数的思想和搜索二叉树一毛一样,四种情况

    • kv.first大于当前节点,往右走。
    • kv.first小于当前节点,往左走。
    • kv.first等于当前节点的值,找到了。
    • 走到空也没找到,返回false。
    1. //Find查找
    2. bool Find(const K& key)
    3. {
    4. Node* cur = _root;
    5. while (cur)
    6. {
    7. if (cur->_key < key)
    8. {
    9. cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
    10. }
    11. else if (cur->_key > key)
    12. {
    13. cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
    14. }
    15. else
    16. {
    17. return true;//查找成功,返回true。
    18. }
    19. }
    20. return false;//没找到返回false
    21. }

    xdm,删除操作就不讲了,一个插入就这么费劲了,再说删除的效率特别低,我现在都开上BBC了你让我骑自行车,肯定不行啊。

    还是那句话,代码是手敲的,难免出现一些错误,调试没发现,如果各位找到,评论区说一下,我及时改正。 

  • 相关阅读:
    金丝桃素白蛋白纳米粒-大肠杆菌血清抗体复合物|凝血因子Ⅷ人血清白蛋白HSA纳米粒|地奥司明人血清白蛋白纳米粒
    numpy矩阵画框框
    Hive 分桶表
    电脑软件:推荐一款非常实用的固态硬盘优化工具
    IPC protocol for local host
    声网赵斌:RTE 体验提升,新一代 Killer App 将成为现实丨RTE 2022
    Kotlin 数据类生成多个构造函数
    在Winform开发中,使用Async-Awati异步任务处理代替BackgroundWorker
    彻底了解线程池的原理——40行从零开始自己写线程池
    jQuery实现下拉菜单
  • 原文地址:https://blog.csdn.net/bit_jie/article/details/127682871