• 【 C++ 】AVL树


    目录

    1、底层结构

    2、AVL树的概念

    3、AVL树节点的定义

    4、基本框架

    5、AVL树的插入

    6、AVL树的旋转

            左单旋

            右单旋

            左右双旋

            右左双旋

    7、AVL树的验证

    8、AVL树的查找

    9、AVL树的删除(了解)

    10、AVL树的性能

    11、源码链接


    1、底层结构

    前面对map、multimap、set、multiset进行了简单的介绍,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。


    2、AVL树的概念

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

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

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

    下面给出一幅图来判断是否为AVL树:

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


    3、AVL树节点的定义

    这里我们实现的AVL树为KV模型,自然节点的模板参数有两个,并且节点定义为三叉连结构(左孩子,右孩子,父亲),在二叉链表的基础上加了一个指向父结点的指针域,使得即便于查找孩子结点,又便于查找父结点。接着还需要创建一个变量_bf作为平衡因子右子树 - 左子树的高度差)。最后写一个构造函数初始化变量即可。

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

    4、基本框架

    此部分内容为AVL树的类,主要作用是来完成后续的插入旋转删除……操作:

    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. };

    5、AVL树的插入

    插入主要分为这几大步骤:

    • 1、一开始为空树,直接new新节点
    • 2、一开始非空树,寻找插入的合适位置
    • 3、找到插入的合适位置后,进行父亲与孩子的双向链接
    • 4、更新新插入的节点祖先的平衡因子
    • 5、针对不合规的平衡因子进行旋转调整

    接下来对其进行逐个分析:

    • 1、一开始为空树,直接new新节点:

    因为树为空的,所以直接new一个新插入的节点,将其作为根_root即可,接着更新平衡因子_bf为0,最后返回true。

    • 2、一开始非空树,寻找插入的合适位置:

    这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:

    1. 插入的值 > 节点的值,更新到右子树查找
    2. 插入的值 < 节点的值,更新到左子树查找
    3. 插入的值 = 节点的值,数据冗余插入失败,返回false

    循环结束的时候,就说明已经找到插入的合适位置,即可进行下一步链接。

    • 3、找到插入的合适位置后,进行父亲与孩子的双向链接:

    注意这里节点的构成为三叉链,因此最后链接后端孩子和父亲是双向链接,具体操作如下:

    1. 插入的值 > 父亲的值,把插入的值链接在父亲的右边
    2. 插入的值 < 父亲的值,把插入的值链接在父亲的左边
    3. 因为是三叉连,插入后记得双向链接(孩子链接父亲)

    走到这,说明节点已经插入完毕,但是接下来就要更新平衡因子了

    • 4、更新新插入的节点祖先的平衡因子:

    当我们插入新节点后,子树的高度可能会发生变化,针对这一变化,我们给出以下要求:

    1. 子树的高度变了,就要继续往上更新
    2. 子树的高度不变,则更新完成
    3. 子树违反平衡规则(平衡因子的绝对值 >= 2),则停止更新,需要旋转子树进行调整

    具体的更新规则如下:

    1. 新增结点在parent的右边,parent的平衡因子++
    2. 新增结点在parent的左边,parent的平衡因子 --

    每更新完一个节点的平衡因子后,都要进行如下判断:

    1. 如果parent的平衡因子等于-1或者1(说明原先是0,左右登高,插入节点后使左子树或右子树增高了)。表明还需要继续往上更新平衡因子
    2. 如果parent的平衡因子等于0(说明原先是1或-1,一高一低,插入节点后填上了矮的那一方)表明无需更新平衡因子了。
    3. 如果parent的平衡因子等于-2或者2(说明原先是1或-1,一高一低,插入节点后填上了高的那一方),表明此时以parent结点为根结点的子树已经不平衡了需要进行旋转处理

    图示如下:

    • 5、针对不合规的平衡因子进行旋转调整:

    当父亲parent的平衡因子为2或-2时,就要进行旋转调整了,而又要分为以下4类进行旋转:

    1. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
    2. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
    3. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
    4. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。

    代码如下:

    1. //Insert插入
    2. bool Insert(const pair& kv)
    3. {
    4. //1、一开始为空树,直接new新节点
    5. if (_root == nullptr)
    6. {
    7. //如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf
    8. _root = new Node(kv);
    9. _root->_bf = 0;
    10. return true;
    11. }
    12. //2、寻找插入的合适位置
    13. Node* cur = _root;//记录插入的位置
    14. Node* parent = nullptr;//保存parent为cur的父亲
    15. while (cur)
    16. {
    17. if (cur->_kv.first < kv.first)
    18. {
    19. //插入的值 > 节点的值
    20. parent = cur;
    21. cur = cur->_right;//更新到右子树查找
    22. }
    23. else if (cur->_kv.first > kv.first)
    24. {
    25. //插入的值 < 节点的值
    26. parent = cur;
    27. cur = cur->_left;//更新到左子树查找
    28. }
    29. else
    30. {
    31. //插入的值 = 节点的值,数据冗余插入失败,返回false
    32. return false;
    33. }
    34. }
    35. //3、找到了插入的位置,进行父亲与插入节点的链接
    36. cur = new Node(kv);
    37. if (parent->_kv.first < kv.first)
    38. {
    39. //插入的值 > 父亲的值,链接在父亲的右边
    40. parent->_right = cur;
    41. }
    42. else
    43. {
    44. //插入的值 < 父亲的值,链接在父亲的左边
    45. parent->_left = cur;
    46. }
    47. //因为是三叉连,插入后记得双向链接(孩子链接父亲)
    48. cur->_parent = parent;
    49. //4、更新新插入节点的祖先的平衡因子
    50. while (parent)//最远要更新到根
    51. {
    52. if (cur == parent->_right)
    53. {
    54. parent->_bf++;//新增结点在parent的右边,parent的平衡因子++
    55. }
    56. else
    57. {
    58. parent->_bf--;//新增结点在parent的左边,parent的平衡因子 --
    59. }
    60. //判断是否继续更新?
    61. if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方
    62. {
    63. //1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新
    64. break;
    65. }
    66. else if (parent->_bf == 1 || parent->_bf == -1)
    67. {
    68. //0 -》 1或-1 此时说明插入节点导致一边变高了,继续更新祖先
    69. cur = cur->_parent;
    70. parent = parent->_parent;
    71. }
    72. else if (parent->_bf == 2 || parent->_bf == -2)
    73. {
    74. //1 or -1 -》2或-2 插入节点导致本来高的一边又变更高了
    75. //此时子树不平衡,需要进行旋转
    76. if (parent->_bf == 2 && cur->_bf == 1)
    77. {
    78. RotateL(parent);//右边高,左单旋
    79. }
    80. else if (parent->_bf == -2 && cur->_bf == -1)
    81. {
    82. RotateR(parent);//左边高,右单旋
    83. }
    84. else if (parent->_bf == -2 && cur->_bf == 1)
    85. {
    86. RotateLR(parent);//左右双旋
    87. }
    88. else if (parent->_bf == 2 && cur->_bf == -1)
    89. {
    90. RotateRL(parent);//右左双旋
    91. }
    92. break;
    93. }
    94. else
    95. {
    96. //插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点
    97. //实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题
    98. assert(false);
    99. }
    100. }
    101. return true;
    102. }

    6、AVL树的旋转

    AVL树的旋转分为4种:

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

    AVL树的旋转要遵循下面两个原则:

    • 1、保持搜索树的规则
    • 2、子树变平衡

    左单旋

    • 条件:新节点插入较高右子树的右侧

    图示:

    鉴于左单旋的情况非常多,这里我们画一张抽象图来演示:

    这里的长方形条(a、b、c表示的是子树h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:

    1. 让subRL变成parent的右子树,更新subRL的父亲为parent
    2. 让subR变成根节点
    3. 让parent变成subR的左子树,更新parent的父亲为subR
    4. 更新平衡因子

    注意:

    1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subR
    2. subRL可能为空,但是更新subRL的父亲为parent是建立在subRL不为空的前提下完成的。

    解释为何上述左单旋的可行性:

    • 首先,根据底层二叉搜索树的结构:b节点的值肯定是在30~60之间的,b去做30的右子树没有任何问题,且这里把60挪到根部,随即把30作为60的右子树,这样整体的变化就像是一个左旋一样,而且也满足二叉搜索树的性质且均平衡。

    代码如下:

    1. void RotateL(Node* parent)
    2. {
    3. Node* subR = parent->_right;
    4. Node* subRL = subR->_left;
    5. Node* ppNode = parent->_parent;//提前保持parent的父亲
    6. //1、建立parent和subRL之间的关系
    7. parent->_right = subRL;
    8. if (subRL)//防止subRL为空
    9. {
    10. subRL->_parent = parent;
    11. }
    12. //2、建立subR和parent之间的关系
    13. subR->_left = parent;
    14. parent->_parent = subR;
    15. //3、建立ppNode和subR之间的关系
    16. if (parent == _root)
    17. {
    18. _root = subR;
    19. _root->_parent = nullptr;
    20. }
    21. else
    22. {
    23. if (parent == ppNode->_left)
    24. {
    25. ppNode->_left = subR;
    26. }
    27. else
    28. {
    29. ppNode->_right = subR;
    30. }
    31. subR->_parent = ppNode;//三叉链双向链接关系
    32. }
    33. //4、更新平衡因子
    34. subR->_bf = parent->_bf = 0;
    35. }

    右单旋

    • 条件:新节点插入较高左子树的左侧

    图示:

    同样这里的长方形条(a、b、c表示的是子树h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:

    1. 让subLR变成parent的左子树,更新subLR的父亲为parent
    2. 让subL变成根节点
    3. 让parent变成subL的右子树,更新parent的父亲为subL
    4. 更新平衡因子

    注意:

    1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subL
    2. subLR可能为空,但是更新subLR的父亲为parent是建立在subLR不为空的前提下完成的。

    代码如下:

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

    左右双旋

    • 条件:新节点插入较高左子树的右侧

    接下来图示解析左右双旋的具体解法。

    • 1、插入新节点:

    此类模型既不满足左单旋的条件也不满足右单旋的条件,但是我们可以将其组合起来,即左右双旋先左单旋,再右单旋)的办法重新建立平衡。接下来执行下一步左单旋

    • 2、以节点30(subL)为旋转点左单旋:

    此时再观察这幅图,这部就是一个妥妥的右单旋模型吗,把60的左子树看成一个整体,此时新插入的节点即插入较高左子树的左侧,刚好符合右单旋的性质,接下来即可进行右单旋

    • 3、以节点90(parent)为旋转点进行右单旋:

    此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

    • 1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为100

    • 2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0-10

    • 3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为000。  

    这里可以看出唯有subLR平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

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

    右左双旋

    • 条件:新节点插入较高右子树的左侧

    接下来图示解析左右双旋的具体解法。

    • 1、插入新节点:

    注意这里的新节点插在了较高右子树的左侧,不能用上文的单旋转以及左右旋转,相反而应使用右左旋转先右单旋再左单旋)来解决,接下来执行下一步右单旋:

    • 2、以节点90(subR)为旋转点右单旋:

    此时再观察这幅图,这部就是一个妥妥的左单旋模型吗,把60的右子树看成一个整体,此时新插入的节点即插入较高右子树的右侧,刚好符合左单旋的性质,接下来即可进行左单旋

    • 3、以节点30(parent)为旋转点左单旋:

    此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

    • 1、当subRL原始平衡因子是-1时,右左双旋后parent、subR、subRL的平衡因子分别更新为010

    • 2、当subRL原始平衡因子是1时,右左双旋后parent、subR、subRL的平衡因子分别更新为-100

    • 3、当subRL原始平衡因子是0时,右左双旋后parent、subR、subRL的平衡因子分别更新为000。 

    这里可以看出唯有subRL平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

    • 代码如下:
    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. }

    7、AVL树的验证

    AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,就是看它是否为二叉搜索树,以及是否为一颗平衡树。接下来分别讨论:

    • 1、验证其为二叉搜索树:

    这里我们只需要进行中序遍历看看结果是否可得到一个有序的序列,如果可以则证明是二叉搜索树,而中序遍历的实现非常简单,先前的二叉搜索树的实现已然完成过,这里直接给出代码:

    1. //中序遍历的子树
    2. void _InOrder(Node* root)
    3. {
    4. if (root == nullptr)
    5. return;
    6. _InOrder(root->_left);
    7. cout << root->_kv.first << " ";
    8. _InOrder(root->_right);
    9. }
    10. //中序遍历
    11. void InOrder()
    12. {
    13. _InOrder(_root);
    14. cout << endl;
    15. }

    检测是否为二叉搜索树的代码实现后,接下来开始验证是否为平衡树。

    • 2、验证其为平衡树:

    规则如下:(递归的思想)

    1. 空树也是平衡树,一开始就要判断
    2. 封装一个专门计算高度的函数(递归计算高度)后续用来计算高度差平衡因子diff)
    3. 如过diff不等于root的平衡因子(root->_bf),或root平衡因子的绝对值超过1,则一定不是AVL树
    4. 继续递归到子树&&右树,直至结束

    代码如下:

    1. //验证一棵树是否为平衡树
    2. bool IsBalanceTree()
    3. {
    4. return _IsBalanceTree(_root);
    5. }
    6. //判读是否平衡的子树
    7. bool _IsBalanceTree(Node* root)
    8. {
    9. //空树也是AVL树
    10. if (nullptr == root)
    11. return true;
    12. //计算root节点的平衡因子diff:即root左右子树的高度差
    13. int leftHeight = _Height(root->_left);
    14. int rightHeight = _Height(root->_right);
    15. int diff = rightHeight - leftHeight;
    16. //如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树
    17. if ((abs(diff) > 1))
    18. {
    19. cout << root->_kv.first << "节点平衡因子异常" << endl;
    20. return false;
    21. }
    22. if (diff != root->_bf)
    23. {
    24. cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;
    25. return false;
    26. }
    27. //继续递归检测,直到结束
    28. return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
    29. }
    30. //求高度的子树
    31. int _Height(Node* root)
    32. {
    33. if (root == nullptr)
    34. return 0;
    35. int lh = _Height(root->_left);
    36. int rh = _Height(root->_right);
    37. return lh > rh ? lh + 1 : rh + 1;
    38. }

    综合上述两大步骤的操作即可对一棵树充分的验证是否为AVL树。


    8、AVL树的查找

    Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:

    1. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
    2. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
    3. 若key值等于当前结点的值,则查找成功,返回true。
    4. 若遍历一圈cur走到nullptr了说明没有此结点,返回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;//若key值等于当前结点的值,则查找成功,返回true。
    18. }
    19. }
    20. return false;//遍历一圈没找到返回false
    21. }

    9、AVL树的删除(了解)

    因为AVL树也是二叉搜索树,主要是三个大思路:

    1. 按二叉搜索树的规则删除
    2. 更新平衡因子
    3. 出现不平衡,需要旋转调整

    只不过与搜索二叉树的删除不同的是,删除节点后的平衡因子需要不断更新,最差情况下一直要调整到根节点的位置。具体这里就不再实现了,因为着实有点复杂。不过《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版这两本书上是有详细的讲解的哈。


    10、AVL树的性能

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


    11、源码链接

    链接直达:AVL树的模拟实现完整版

  • 相关阅读:
    surging作者出具压测结果
    从零开始C语言精讲篇2:分支与循环
    NeRF in the Wild: Neural Radiance Fields for Unconstrained Photo Collections
    什么是模型监控?(Valohai)
    高并发Server的基石:reactor反应堆模式
    使用gojs画出拓扑图,清晰的看出组织人员关系
    Node.js学习二 —— 缓冲区
    AI+医疗:使用神经网络进行医学影像识别分析
    ns-3 多天线设置与ns-3信道设置
    【Android】WebView 基本使用
  • 原文地址:https://blog.csdn.net/bit_zyx/article/details/126260295