1. 为什么平衡树?

在二叉搜索树(BST,Binary Search Tree)中提到,BST树可能会退化成一个链表(整棵树中只有左子树,或者只有右子树),这将大大影响二叉树的性能。

前苏联科学家G.M. Adelson-Velskii 和 E.M. Landis给出了答案。他们在1962年发表的一篇名为《An algorithm for the organization of information》的文章中提出了一种自平衡二叉查找树(self-balancing binary search tree)。这种二叉查找树在插入和删除操作中,可以通过一系列的旋转操作来保持平衡,从而保证了二叉查找树的查找效率。最终这种二叉查找树以他们的名字命名为“AVL-Tree”,它也被称为平衡二叉树(Balanced Binary Tree)。

2. 原理

在节点上设置一个平衡因子BF,代表左右子树的高度差,BF = { -1, 0, 1}。

3. 旋转

AVL的Insert/Delete操作可能会引起树的失衡,可以通过选择解决这个问题。

3.1 4种旋转

(1)LL

(2)RR

(3)LR

(4)RL

在下面的文章中有一个关于AVL选择的动画,大家不妨看看。

C#与数据结构--树论--平衡二叉树(AVL Tree)

3.2 旋转实现

在算法导论中给出旋转的伪代码:

  1. LEFT-ROTATE(T, x)
  2. 1 y right[x] Set y.
  3. 2 right[x] left[y] Turn y's left subtree into x's right subtree.
  4. 3 p[left[y]] x
  5. 4 p[y] p[x] Link x's parent to y.
  6. 5 if p[x] = nil[T]
  7. 6 then root[T] ← y
  8. 7 else if x = left[p[x]]
  9. 8 then left[p[x]] ← y
  10. 9 else right[p[x]] ← y
  11. 10 left[y] ← x ▹ Put x on y's left.
  12. 11 p[x] y
  1. //旋转以root为根的子树,当高度改变,则返回true;高度未变则返回false
  2. private bool RotateSubTree(int bf)
  3. {
  4. bool tallChange = true;
  5. Node root = path[p], newRoot = null;
  6. if (bf == 2) //当平衡因子为2时需要进行旋转操作
  7. {
  8. int leftBF = root.Left.BF;
  9. if (leftBF == -1) //LR型旋转
  10. {
  11. newRoot = LR(root);
  12. }
  13. else if (leftBF == 1)
  14. {
  15. newRoot = LL(root); //LL型旋转
  16. }
  17. else //当旋转根左孩子的bf为0时,只有删除时才会出现
  18. {
  19. newRoot = LL(root);
  20. tallChange = false;
  21. }
  22. }
  23. if (bf == -2) //当平衡因子为-2时需要进行旋转操作
  24. {
  25. int rightBF = root.Right.BF; //获取旋转根右孩子的平衡因子
  26. if (rightBF == 1)
  27. {
  28. newRoot = RL(root); //RL型旋转
  29. }
  30. else if (rightBF == -1)
  31. {
  32. newRoot = RR(root); //RR型旋转
  33. }
  34. else //当旋转根左孩子的bf为0时,只有删除时才会出现
  35. {
  36. newRoot = RR(root);
  37. tallChange = false;
  38. }
  39. }
  40. //更改新的子树根
  41. if (p > 0)
  42. {
  43. if (root.Data < path[p - 1].Data)
  44. {
  45. path[p - 1].Left = newRoot;
  46. }
  47. else
  48. {
  49. path[p - 1].Right = newRoot;
  50. }
  51. }
  52. else
  53. {
  54. _head = newRoot; //如果旋转根为AVL树的根,则指定新AVL树根结点
  55. }
  56. return tallChange;
  57. }
  58. //root为旋转根,rootPrev为旋转根双亲结点
  59. private Node LL(Node root) //LL型旋转,返回旋转后的新子树根
  60. {
  61. Node rootNext = root.Left;
  62. root.Left = rootNext.Right;
  63. rootNext.Right = root;
  64. if (rootNext.BF == 1)
  65. {
  66. root.BF = 0;
  67. rootNext.BF = 0;
  68. }
  69. else //rootNext.BF==0的情况,删除时用
  70. {
  71. root.BF = 1;
  72. rootNext.BF = -1;
  73. }
  74. return rootNext; //rootNext为新子树的根
  75. }
  76. private Node LR(Node root) //LR型旋转,返回旋转后的新子树根
  77. {
  78. Node rootNext = root.Left;
  79. Node newRoot = rootNext.Right;
  80. root.Left = newRoot.Right;
  81. rootNext.Right = newRoot.Left;
  82. newRoot.Left = rootNext;
  83. newRoot.Right = root;
  84. switch (newRoot.BF) //改变平衡因子
  85. {
  86. case 0:
  87. root.BF = 0;
  88. rootNext.BF = 0;
  89. break;
  90. case 1:
  91. root.BF = -1;
  92. rootNext.BF = 0;
  93. break;
  94. case -1:
  95. root.BF = 0;
  96. rootNext.BF = 1;
  97. break;
  98. }
  99. newRoot.BF = 0;
  100. return newRoot; //newRoot为新子树的根
  101. }
  102. private Node RR(Node root) //RR型旋转,返回旋转后的新子树根
  103. {
  104. Node rootNext = root.Right;
  105. root.Right = rootNext.Left;
  106. rootNext.Left = root;
  107. if (rootNext.BF == -1)
  108. {
  109. root.BF = 0;
  110. rootNext.BF = 0;
  111. }
  112. else //rootNext.BF==0的情况,删除时用
  113. {
  114. root.BF = -1;
  115. rootNext.BF = 1;
  116. }
  117. return rootNext; //rootNext为新子树的根
  118. }
  119. private Node RL(Node root) //RL型旋转,返回旋转后的新子树根
  120. {
  121. Node rootNext = root.Right;
  122. Node newRoot = rootNext.Left;
  123. root.Right = newRoot.Left;
  124. rootNext.Left = newRoot.Right;
  125. newRoot.Right = rootNext;
  126. newRoot.Left = root;
  127. switch (newRoot.BF) //改变平衡因子
  128. {
  129. case 0:
  130. root.BF = 0;
  131. rootNext.BF = 0;
  132. break;
  133. case 1:
  134. root.BF = 0;
  135. rootNext.BF = -1;
  136. break;
  137. case -1:
  138. root.BF = 1;
  139. rootNext.BF = 0;
  140. break;
  141. }
  142. newRoot.BF = 0;
  143. return newRoot; //newRoot为新子树的根
  144. }

4. 插入与删除

4.1 插入

  1. public bool Add(int value) //添加一个元素
  2. { //如果是空树,则新结点成为二叉排序树的根
  3. if (_head == null)
  4. {
  5. _head = new Node(value);
  6. _head.BF = 0;
  7. return true;
  8. }
  9. p = 0;
  10. //prev为上一次访问的结点,current为当前访问结点
  11. Node prev = null, current = _head;
  12. while (current != null)
  13. {
  14. path[p++] = current; //将路径上的结点插入数组
  15. //如果插入值已存在,则插入失败
  16. if (current.Data == value)
  17. {
  18. return false;
  19. }
  20. prev = current;
  21. //当插入值小于当前结点,则继续访问左子树,否则访问右子树
  22. current = (value < prev.Data) ? prev.Left : prev.Right;
  23. }
  24. current = new Node(value); //创建新结点
  25. current.BF = 0;
  26. if (value < prev.Data) //如果插入值小于双亲结点的值
  27. {
  28. prev.Left = current; //成为左孩子
  29. }
  30. else //如果插入值大于双亲结点的值
  31. {
  32. prev.Right = current; //成为右孩子
  33. }
  34. path[p] = current; //将新元素插入数组path的最后
  35. //修改插入点至根结点路径上各结点的平衡因子
  36. int bf = 0;
  37. while (p > 0)
  38. { //bf表示平衡因子的改变量,当新结点插入左子树,则平衡因子+1
  39. //当新结点插入右子树,则平衡因子-1
  40. bf = (value < path[p - 1].Data) ? 1 : -1;
  41. path[--p].BF += bf; //改变当父结点的平衡因子
  42. bf = path[p].BF; //获取当前结点的平衡因子
  43. //判断当前结点平衡因子,如果为0表示该子树已平衡,不需再回溯
  44. //而改变祖先结点平衡因子,此时添加成功,直接返回
  45. if (bf == 0)
  46. {
  47. return true;
  48. }
  49. else if (bf == 2 || bf == -2) //需要旋转的情况
  50. {
  51. RotateSubTree(bf);
  52. return true;
  53. }
  54. }
  55. return true;
  56. }

4.2 删除

  1. private void RemoveNode(Node node)
  2. {
  3. Node tmp = null;
  4. //当被删除结点存在左右子树时
  5. if (node.Left != null && node.Right != null)
  6. {
  7. tmp = node.Left; //获取左子树
  8. path[++p] = tmp;
  9. while (tmp.Right != null) //获取node的中序遍历前驱结点,并存放于tmp中
  10. { //找到左子树中的最右下结点
  11. tmp = tmp.Right;
  12. path[++p] = tmp;
  13. }
  14. //用中序遍历前驱结点的值代替被删除结点的值
  15. node.Data = tmp.Data;
  16. if (path[p - 1] == node)
  17. {
  18. path[p - 1].Left = tmp.Left;
  19. }
  20. else
  21. {
  22. path[p - 1].Right = tmp.Left;
  23. }
  24. }
  25. else //当只有左子树或右子树或为叶子结点时
  26. { //首先找到惟一的孩子结点
  27. tmp = node.Left;
  28. if (tmp == null) //如果只有右孩子或没孩子
  29. {
  30. tmp = node.Right;
  31. }
  32. if (p > 0)
  33. {
  34. if (path[p - 1].Left == node)
  35. { //如果被删结点是左孩子
  36. path[p - 1].Left = tmp;
  37. }
  38. else
  39. { //如果被删结点是右孩子
  40. path[p - 1].Right = tmp;
  41. }
  42. }
  43. else //当删除的是根结点时
  44. {
  45. _head = tmp;
  46. }
  47. }
  48. //删除完后进行旋转,现在p指向实际被删除的结点
  49. int data = node.Data;
  50. while (p > 0)
  51. { //bf表示平衡因子的改变量,当删除的是左子树中的结点时,平衡因子-1
  52. //当删除的是右子树的孩子时,平衡因子+1
  53. int bf = (data <= path[p - 1].Data) ? -1 : 1;
  54. path[--p].BF += bf; //改变当父结点的平衡因子
  55. bf = path[p].BF; //获取当前结点的平衡因子
  56. if (bf != 0) //如果bf==0,表明高度降低,继续后上回溯
  57. {
  58. //如果bf为1或-1则说明高度未变,停止回溯,如果为2或-2,则进行旋转
  59. //当旋转后高度不变,则停止回溯
  60. if (bf == 1 || bf == -1 || !RotateSubTree(bf))
  61. {
  62. break;
  63. }
  64. }
  65. }
  66. }

平衡树(AVL)详解的更多相关文章

  1. 详解什么是平衡二叉树(AVL)(修订补充版)

    详解什么是平衡二叉树(AVL)(修订补充版) 前言 Wiki:在计算机科学中,AVL树是最早被发明的自平衡二叉查找树.在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树.查 ...

  2. 数据结构图文解析之:AVL树详解及C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  3. AVL树详解

    AVL树 参考了:http://www.cppblog.com/cxiaojia/archive/2012/08/20/187776.html 修改了其中的错误,代码实现并亲自验证过. 平衡二叉树(B ...

  4. AVL树平衡旋转详解

    AVL树平衡旋转详解 概述 AVL树又叫做平衡二叉树.前言部分我也有说到,AVL树的前提是二叉排序树(BST或叫做二叉查找树).由于在生成BST树的过程中可能会出现线型树结构,比如插入的顺序是:1, ...

  5. Redis数据类型使用场景及有序集合SortedSet底层实现详解

    Redis常用数据类型有字符串String.字典dict.列表List.集合Set.有序集合SortedSet,本文将简单介绍各数据类型及其使用场景,并重点剖析有序集合SortedSet的实现. Li ...

  6. Java集合详解6:TreeMap和红黑树

    Java集合详解6:TreeMap和红黑树 初识TreeMap 之前的文章讲解了两种Map,分别是HashMap与LinkedHashMap,它们保证了以O(1)的时间复杂度进行增.删.改.查,从存储 ...

  7. 探索Redis设计与实现6:Redis内部数据结构详解——skiplist

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  8. 【转】Redis内部数据结构详解 -- skiplist

    本文是<Redis内部数据结构详解>系列的第六篇.在本文中,我们围绕一个Redis的内部数据结构--skiplist展开讨论. Redis里面使用skiplist是为了实现sorted s ...

  9. 丰富图文详解B-树原理,从此面试再也不慌

    本文始发于个人公众号:TechFlow,原创不易,求个关注 本篇原计划在上周五发布,由于太过硬核所以才拖到了这周五.我相信大家应该能从标题当中体会到这个硬核. 周五的专题是大数据和分布式,我最初的打算 ...

  10. 数据结构图文解析之:队列详解与C++模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

随机推荐

  1. 关于UIScrollView属性和方法的总结

    iOS中UIScollView的总结 在iOS开发中可以说UIScollView是所有滑动类视图的基础,包括UITableView,UIWebView,UICollectionView等等,UIScr ...

  2. mac 连接linux

    1.ssh 通过ssh 的方式直接连接linux ssh name@ip -22 例:ssh  zyc@192.168.1.100 -22 这个的前提是linux 要开启ssh 服务 先看一下linu ...

  3. C++中new的用法

    new int;//开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针) new int(100);//开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址 ...

  4. 【Ural1057】幂和的数量

    [题目描述] 写一个程序来计算区间[X,Y]内满足如下条件的整数个数:它恰好等于K个互不相等的B的整数幂之和. 举个例子.令X=15,Y=20,K=2,B=2.在这个例子中,区间[15,20]内有3个 ...

  5. Extjs4 关于Store的一些操作(转)

    1.关于加载和回调的问题 ExtJs的Store在加载时候一般是延迟加载的,这时候Grid就会先出现一片空白,等加载完成后才出现数据:因此,我们需要给它添加一个提示信息! 但是Store却没有wait ...

  6. 『重构--改善既有代码的设计』读书笔记----Extract Class

    在面向对象中,对于类这个概念我们应该有一个清晰的责任认识,就是每个类应该只有一个变化点,每个类的变化应该只受到单一的因素,即每个类应该只有一个明确的责任.当然了,说时容易做时难,很多人可能都会和我一样 ...

  7. 16_用LVM扩展xfs文件系统(当分区空间不够时)

    1. 查看当前卷组空间(volume group)使用情况 [root@localhost ~]# vgdisplay 从下面的代码中发现剩余空间为0 --- Volume group --- VG ...

  8. Eclipse启动Tomcat报错,系统缺少本地apr库

    Eclipse启动Tomcat报错,系统缺少本地apr库. Tomcat中service.xml中的设置情况. 默认情况是HTTP协议的值:protocol="HTTP/1.1" ...

  9. Python自动化运维之29、Bottle框架

    Bottle 官网:http://bottlepy.org/docs/dev/index.html Bottle是一个快速.简洁.轻量级的基于WSIG的微型Web框架,此框架只由一个 .py 文件,除 ...

  10. 七天学会 SALT STACK 自动化运维 (1)

    七天学会 SALT STACK 自动化运维 (1) 简单理解 SALTSTACK 安装与配置 基本的使用方法 结束语 引用资源 简单理解 SALT STACK 笔者是初次接触 自动化运维 这一技术领域 ...