• 图解AVLTree


    本篇文章我们将通过模拟实现AVLTree的插入来帮助大家理解AVLTree的原理。

    AVLTree简单介绍    

     AVLTree即高度平衡二叉搜索树,其本质上是带了平衡功能的二叉搜索树,与普通二叉搜索树不同之处在于AVL树中任何节点的两个子树的高度最大差别为1,因为其高度平衡的特点使得其搜索的时间复杂度稳定在O(logN)。优化了普通的二叉搜索树在数据接近有序的情况下搜索的时间复杂度接近于O(N)的缺陷。

    AVLTree的结点

    AVLTrees的结点是由一个三叉链结构,由三个指针分别指向左子树,右子树和父节点。其数据域为pair类型。我们利用pair储存键值对,键值对(“key = value”),顾名思义,每一个键会对应一个值。pair有两个成员first,second,我们用frist储存key,second储存value。在排序时我们只关注key的大小。同时为了保证AVLTree的平衡我们引入一个概念——平衡因子。平衡因子是该节点的左子树高度与右子树高度之差。平衡因子有且仅有五个值{-2,-1,0,1,2},当平衡因子为-2或2时代表该结点失去平衡需要调整.

    1. //定义节点
    2. template<class K, class V>
    3. struct AVLTreeNode
    4. {
    5. //节点的构造
    6. AVLTreeNode(const std::pair kv)
    7. :_left(nullptr)
    8. ,_right(nullptr)
    9. ,_parent(nullptr)
    10. ,_kv(kv)
    11. ,_bf(0)
    12. {}
    13. AVLTreeNode* _left; //左子树
    14. AVLTreeNode* _right; //右子树
    15. AVLTreeNode* _parent;//父节点
    16. int _bf; //平衡因子balance factor
    17. std::pair _kv;
    18. };
    AVLTree的插入

    接下来我们讨论AVLTree的插入,我们将AVLTree的插入分为三步

    一,按照二叉搜素树的逻辑进行插入。

    二,更新平衡因子。

    三,对失衡处进行旋转处理。

    1. 插入

    首先我们将插入结点按照二叉搜索树逻辑进行插入。代码如下:

    1. bool insert(const std::pair<K, V>& kv)
    2. {
    3. //插入
    4. if (_root == nullptr)
    5. {
    6. _root = new Node(kv);
    7. return true;
    8. }
    9. Node* cur = _root;
    10. Node* parent = nullptr;
    11. //寻找结点应该在的位置
    12. while (cur)
    13. {
    14. if (cur->_kv.first < kv.first)
    15. {
    16. parent = cur;
    17. cur = cur->_right;
    18. }
    19. else if (cur->_kv.first > kv.first)
    20. {
    21. parent = cur;
    22. cur = cur->_left;
    23. }
    24. //树中存在相同结点插入失败
    25. else
    26. {
    27. return false;
    28. }
    29. }
    30. cur = new Node(kv);
    31. //插入
    32. if (parent->_kv.first > kv.first)
    33. {
    34. parent->_left = cur;
    35. cur->_parent = parent;
    36. }
    37. else
    38. {
    39. parent->_right = cur;
    40. cur->_parent = parent;
    41. }
    42. //更新平衡因子
    43. ......
    44. }

    我们对该代码进行分析,首先我们处理树为空的情况,当根节点为空时该二叉树为空,此时我们将待插入结点设为根结点

    1. if (_root == nullptr)
    2. {
    3. _root = new Node(kv);
    4. return true;
    5. }

    接下来我们处理树不为空的情况,我们定义两个指针cur和parent。cur首先指向根节点,parent指向空。我们将待插入pair对象的frist与cur指向结点的pair的frist比较,按照二叉搜索树的规则(左子树上所有结点的值均小于它的根结点的值,右子树上所有结点的值均大于它的根结点的值)当kv.first < cur->_kv.first时,kv应该在cur的左子树上插入,当kv.first > cur->_kv.first时,kv应该在cur的右子树上插入。根据比较的结果我们让cur指向它的左子树或右子树,并用parent记录cur的父节点。循环该操作,当cur指向空时,parent就是待插入结点的父结点,接下来我们通过比较kv.frist与parent->_kv.first,确定结点的最终位置。代码如下:

    1. Node* cur = _root;
    2. Node* parent = nullptr;
    3. //寻找结点应该在的位置
    4. while (cur)
    5. {
    6. if (cur->_kv.first < kv.first)
    7. {
    8. parent = cur;
    9. cur = cur->_right;
    10. }
    11. else if (cur->_kv.first > kv.first)
    12. {
    13. parent = cur;
    14. cur = cur->_left;
    15. }
    16. //树中存在相同结点插入失败
    17. else
    18. {
    19. return false;
    20. }
    21. }
    22. cur = new Node(kv);
    23. //插入
    24. if (parent->_kv.first > kv.first)
    25. {
    26. parent->_left = cur;
    27. cur->_parent = parent;
    28. }
    29. else
    30. {
    31. parent->_right = cur;
    32. cur->_parent = parent;
    33. }
    2.更新平衡因子

    现在我们对平衡因子进行更新,代码如下:

    1. //按二叉搜索树逻辑插入
    2. ........
    3. //更新平衡因子
    4. while (parent)
    5. {
    6. if (cur == parent->_left)
    7. {
    8. parent->_bf--;
    9. }
    10. else
    11. {
    12. parent->_bf++;
    13. }
    14. if (parent->_bf == 0)
    15. {
    16. break;
    17. }
    18. else if (parent->_bf == 1 || parent->_bf == -1)
    19. {
    20. cur = parent;
    21. parent = parent->_parent;
    22. }
    23. else if (parent->_bf == -2 || parent->_bf == 2)
    24. {
    25. //处理失衡部分
    26. ......
    27. }
    28. }

    我们对此代码进行解释,当我们在空位插入结点时只会影响根结点到插入结点路径上结点的平衡因子,其他结点的平衡因子不会改变,如图:

    我们通过判断cur在parent的左子树还是右子树来对平衡因子进行修改,因为_bf=左子树高度-右子树高度;所以当cur == parent->_left时,parent->_bf--; 当cur == parent->_right时,parent->_bf++;
    接下我们对parent的平衡因子,当_bf==0时,说明更新前后该结点的左右子树中的最大高度没有变化,其对祖先结点的平衡因子无影响,更新停止,跳出循环。当_bf=1/-1则说明左右子树中的最大高度改变了,应继续向上更新,于是我们让”  cur = parent;    parent = parent->_parent; “进行迭代处理。当_bf==-2/2说明该节点的左右子树高度差大于1,我们对其进行旋转处理,处理后该节点的左右子树最大高度与插入前相同,于是我们停止更新,跳出循环。

    3.旋转处理

    在该步骤我们将失衡状态归纳为四种:

    这四种情况我们分别进行左单旋处理,右单旋处理 ,左右旋处理和右左旋处理。

    (1).左单旋

    我们将所有需要左单旋的情况抽象为下图:

       

    当parent的平衡因子为2,cur的平衡因子为1时,我们对其进行左旋处理。左旋分为三步:

    1.让cur的左子树成为parent的右子树。

    2.让parent成为cur的左子树。

    3.让parent的父节点指向cur。

    代码如下:

    1. //左单旋
    2. void RotateL(Node* parent)
    3. {
    4. Node* subR = parent->_right;
    5. Node* ppnode = parent->_parent;
    6. Node* subRL = subR->_left;
    7. parent->_right = subRL;
    8. if (subRL)
    9. {
    10. subRL->_parent = parent;
    11. }
    12. subR->_left = parent;
    13. parent->_parent = subR;
    14. if (parent == _root)
    15. {
    16. _root = subR;
    17. subR->_parent = nullptr;
    18. }
    19. else
    20. {
    21. if (parent == ppnode->_left)
    22. {
    23. ppnode->_left = subR;
    24. subR->_parent = ppnode;
    25. }
    26. else
    27. {
    28. ppnode->_right = subR;
    29. subR->_parent = ppnode;
    30. }
    31. }
    32. subR->_bf = parent->_bf = 0;
    33. }

    我们让subR指向parent的右子树,用subRL指向subR的左子树,用ppnode记录parent的父节点。首先我们让parent->_right = subRL;如果subRL不为空,则让subLR的_parent指向parent。若为空则不处理,第一步完成。然后让subR的_left指向parent,让parent的_parent指向subR,第二步完成。第三步有两种情况,parent为根节点,和parent不为根结点。当parent为根节点时,让subR的_parent指向空,当parent不为根节点时,将ppnode与subR进行链接。左单旋完成后parent的_bf与subR的_bf均为0。图解:

     (2).右单旋

    我们将所有需要左单旋的情况抽象为下图:

     当parent的平衡因子为-2,cur的平衡因子为-1时,我们对其进行右旋处理。右旋分为三步:

    1.让cur的右子树成为parent的左子树。

    2.让parent成为cur的右子树。

    3.让parent的父节点指向cur。

    代码如下:

    1. //右单旋
    2. void RotateR(Node* parent)
    3. {
    4. Node* subL = parent->_left;
    5. Node* subLR = subL->_right;
    6. Node* ppnode = parent->_parent;
    7. parent->_left = subLR;
    8. if (subLR)
    9. {
    10. subLR->_parent = parent;
    11. }
    12. subL->_right = parent;
    13. parent->_parent = subL;
    14. if (parent == _root)
    15. {
    16. _root = subL;
    17. subL->_parent = nullptr;
    18. }
    19. else
    20. {
    21. if (parent == ppnode->_left)
    22. {
    23. ppnode->_left = subL;
    24. subL->_parent = ppnode;
    25. }
    26. else
    27. {
    28. ppnode->_right = subL;
    29. subL->_parent = ppnode;
    30. }
    31. }
    32. subL->_bf = parent->_bf = 0;
    33. }

    其代码逻辑参考左单旋,在此不过多赘述。图解:

    (3).左右双旋

    我们将所有需要左右双旋的情况抽象为下图:

    当parent的平衡因子为-2,cur的平衡因子为1时,我们对其进行左右双旋处理。左右双旋分三步:

    一,对cur进行左旋。

    二,对parent进行右旋。

    三,更新平衡因子。

    代码如下:

    1. //左右双旋
    2. void RotateLR(Node* parent)
    3. {
    4. Node* subL = parent->_left;
    5. Node* subLR = subL->_right;
    6. int bf = subLR->_bf;
    7. RotateL(subL);
    8. RotateR(parent);
    9. if (bf == 1)
    10. {
    11. subL->_bf = -1;
    12. }
    13. else if (bf == -1)
    14. {
    15. parent->_bf = 1;
    16. }
    17. }

    前两步我们不过多介绍,我们着重介绍第三步。更新平衡因子时有三种情况。一,subLR->_bf=1.

    二,subLR->_bf=-1。三,subLR的平衡因子为0即subLR为新插入结点。这三种情况下更新后的subL的和parent的平衡因子不同。参考下图:

     (4).右左双旋

    我们将所有需要右左双旋的情况抽象为下图:

    当parent的平衡因子为2,cur的平衡因子为-1时,我们对其进行右左双旋处理。右左双旋分三步:

    一,对cur进行右旋。

    二,对parent进行左旋。

    三,更新平衡因子。

    代码如下:

    1. //右左双旋
    2. void RotateRL(Node* parent)
    3. {
    4. Node* subR = parent->_right;
    5. Node* subRL = subR->_left;
    6. int bf = subRL->_bf;
    7. RotateR(subR);
    8. RotateL(parent);
    9. if (bf == -1)
    10. {
    11. subR->_bf = 1;
    12. }
    13. else if (bf == 1)
    14. {
    15. parent->_bf = -1;
    16. }
    17. }

    代码逻辑同左右双旋。

    小结

    我们对所有代码进行整合:

    1. bool insert(const std::pair<K, V>& kv)
    2. {
    3. if (_root == nullptr)
    4. {
    5. _root = new Node(kv);
    6. return true;
    7. }
    8. Node* cur = _root;
    9. Node* parent = nullptr;
    10. while (cur)
    11. {
    12. if (cur->_kv.first < kv.first)
    13. {
    14. parent = cur;
    15. cur = cur->_right;
    16. }
    17. else if (cur->_kv.first > kv.first)
    18. {
    19. parent = cur;
    20. cur = cur->_left;
    21. }
    22. else
    23. {
    24. return false;
    25. }
    26. }
    27. cur = new Node(kv);
    28. if (parent->_kv.first > kv.first)
    29. {
    30. parent->_left = cur;
    31. cur->_parent = parent;
    32. }
    33. else
    34. {
    35. parent->_right = cur;
    36. cur->_parent = parent;
    37. }
    38. while (parent)
    39. {
    40. if (cur == parent->_left)
    41. {
    42. parent->_bf--;
    43. }
    44. else
    45. {
    46. parent->_bf++;
    47. }
    48. if (parent->_bf == 0)
    49. {
    50. break;
    51. }
    52. else if (parent->_bf == 1 || parent->_bf == -1)
    53. {
    54. cur = parent;
    55. parent = parent->_parent;
    56. }
    57. else if (parent->_bf == -2 || parent->_bf == 2)
    58. {
    59. if (parent->_bf == 2)
    60. {
    61. if (cur->_bf == 1)
    62. {
    63. RotateL(parent);
    64. }
    65. else if (cur->_bf == -1)
    66. {
    67. RotateLR(parent);
    68. }
    69. }
    70. if (parent->_bf == -2)
    71. {
    72. if (cur->_bf == 1)
    73. {
    74. RotateR(parent);
    75. }
    76. else if(cur->_bf == -1)
    77. {
    78. RotateRL(parent);
    79. }
    80. }
    81. break;
    82. }
    83. }
    84. return true;
    85. }
    总结

    以上是我们对AVLTree插入的讲解,感谢您的观看!!!!

  • 相关阅读:
    网络层重点协议——IP协议
    蓝桥杯每日一题20233.10.10
    java计算机毕业设计水星家纺网站源码+系统+数据库+lw文档+mybatis+运行部署
    ARC113D题解
    npm和package.json
    如何学好c++重难点
    [ROS笔记本]QT5问题cmake编译
    APP备案您清楚了吗?
    【Linux进阶篇】Linux安装MySQL
    大数据技术分享 - 话题挑战跳大开团
  • 原文地址:https://blog.csdn.net/2301_80926085/article/details/136115816