下次再让你讲平衡二叉树,可别说不会了

我不是女神ヾ 2023-01-09 12:31 309阅读 0赞

引言

公众号原文链接:下次再让你讲平衡二叉树,可别说不会了 希望点进去的小伙伴关注一下我的公众号哟,文末有二维码,谢谢!

完整项目我已经上传到我的码云git仓库上了,如果就需要的话请访问我的码云git仓库获取,附上地址:https://gitee.com/bobo_tea/datastructure/tree/master/src/main/datastructure/com/bobo/group/tree/avl。或者点击公众号底部菜单->资源汇总->仓库汇总。或者联系我。

1、平衡二叉树基本概念

平衡二叉树,也是一种二叉查找树,但它是平衡的,即左子树与右子树的高度差最多等于1。

平衡二叉树也叫AVL树,取自发明平衡二叉树算法的人的人名。

平衡因子BF:左子树深度减去右子树深度的值。平衡二叉树所有结点的平衡因子只能是-1,0,1。

最小不平衡子树:假设新插入了一个结点A,然后导致树不平衡了,距离结点A最近且BF绝对值大于1的那个结点为B,以B结点为根的树称为最小不平衡子树

2、平衡二叉树实现原理

在构建二叉查找树过程中,每插入一个结点,先检查是否破坏了树的平衡。若是,则找出最小不平衡子树,在保持二叉查找树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之达到新的平衡。

旋转分两种:左旋和右旋。右旋会减小BF,左旋增加BF。

上面的说辞比较抽象,下面就来具体一点的。为了更好地讲解原理,我们以依次插入结点3,2,1,4,5,6,7,10,9,8为例。

第一步

首先插入3和2是没问题的。

第二步

当插入1时,发现结点3的BF变成了2,因此需要右旋,如下图所示。

图片

第三步

插入4没问题。

第四步

当插入5时,结点3的BF变成了-2,因此左旋。

图片

第五步

当插入结点6时,结点2的BF变成了-2,将根结点左旋。但3有点多余,将它设置为结点2的右孩子。

大家有没有担心这种情况:结点4的左子树,不单单是一个结点,而是一颗高度大于1的子树?其实不会的,它只能是一个结点。大家可以用笔在纸上画一下,平衡二叉树不会出现这样的情况。

图片

第六步

当插入结点7时,结点5的BF变成-2,左旋。

图片

第七步

插入10没问题。

第八步

当插入9时,7的BF为-2,理论上左旋就行了,但会不满足二叉查找树的特性。

图片

导致这种情况的原因是:结点7和它的孩子结点10,两者的BF符号不一致。既然不一致,先将结点9和结点10组成的子树右旋,然后再以结点7为根结点进行左旋。

图片

第九步

当插入结点8时,结点6的BF为-2,而结点9的BF为1,为了保证符号统一,因此以结点9为根结点进行右旋,注意,结点8本来是结点7的右孩子,但是右旋之后,因为7<8<9,所以只能将结点8当成结点9的左孩子。

图片

3、平衡二叉树代码实现

平衡二叉树实现原理中的九个步骤,是代码实现的铺垫,建议大家将九个步骤都看懂,然后再看代码,这九个步骤几乎涵盖了所有可能的情况。

我的平衡二叉树实现分四个类:

  • Ancestor:祖先类
  • AVLInsert:平衡二叉树结点插入核心实现
  • AVLMain:main方法所在类,包含示例程序
  • AVLNode:avl树结点类

AVLNode

  1. import com.bobo.group.tree.draw.Drawable;
  2. public class AVLNode implements Drawable {
  3. private int val;
  4. private AVLNode left;
  5. private AVLNode right;
  6. public AVLNode(int val) {
  7. this.val = val;
  8. }
  9. public AVLNode(int val, AVLNode left, AVLNode right) {
  10. this.val = val;
  11. this.left = left;
  12. this.right = right;
  13. }
  14. //省略getter/setter方法
  15. @Override
  16. public String getValue() {
  17. return String.valueOf(this.val);
  18. }
  19. @Override
  20. public Drawable getLeftNode() {
  21. return this.left;
  22. }
  23. @Override
  24. public Drawable getRightNode() {
  25. return this.right;
  26. }
  27. }

Ancestor

  1. /**
  2. * 结点的祖先信息
  3. */
  4. public class Ancestor {
  5. private AVLNode ancestor;
  6. private boolean isLeft;
  7. public Ancestor(AVLNode ancestor) {
  8. this.ancestor = ancestor;
  9. }
  10. public Ancestor(AVLNode ancestor, boolean isLeft) {
  11. this.ancestor = ancestor;
  12. this.isLeft = isLeft;
  13. }
  14. // 省略getter/setter方法
  15. }

AVLInsert

  1. import com.bobo.group.common.CommonUtil;
  2. import com.bobo.group.tree.draw.DrawTree;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. public class AVLInsert {
  6. //记录插入的每个结点
  7. private List<AVLNode> avlNodes = new ArrayList<>();
  8. //画二叉树对象
  9. private DrawTree drawTree = new DrawTree();
  10. //可变的根结点,因为根结点也会旋转
  11. private AVLNode variableRoot;
  12. //生成图片时计数
  13. private int drawCounter=1;
  14. //树偏移量数组,画图用
  15. //private int[] offset_x_arr = new int[]{60,30,25,20,20,20,10,10};
  16. private int[] offset_x_arr = new int[]{120,80,30,30,20,20,10,10};
  17. /**
  18. * 每插入一个结点,就画一次图
  19. * @param key
  20. */
  21. private void drawStep(int key){
  22. drawTree.drawEntrance(variableRoot, CommonUtil.getResourceRoot()+"tree/avl/avlinsert_"+drawCounter+"_"+key+".png",false,offset_x_arr);
  23. drawCounter++;
  24. }
  25. /**
  26. * 平衡二叉树插入结点核心算法
  27. * @param avlNode 当前结点
  28. * @param key 要插入的关键字
  29. * @param ancestorChain 祖先信息链
  30. */
  31. private void insert(AVLNode avlNode,int key,List<Ancestor> ancestorChain){
  32. if(key < avlNode.getVal()){
  33. //新增一个祖先信息
  34. ancestorChain.add(new Ancestor(avlNode,true));
  35. //左孩子为null,则执行插入
  36. if(avlNode.getLeft() == null){
  37. AVLNode keyNode = new AVLNode(key);
  38. avlNodes.add(keyNode);
  39. avlNode.setLeft(keyNode);
  40. //平衡算法
  41. calculateBFAndRotate(ancestorChain);
  42. //平衡后,画图
  43. drawStep(key);
  44. return;
  45. }else{
  46. //递归搜索
  47. insert(avlNode.getLeft(),key,ancestorChain);
  48. }
  49. }
  50. if(key > avlNode.getVal()){
  51. //新增一个祖先信息
  52. ancestorChain.add(new Ancestor(avlNode,false));
  53. //右孩子为null,则执行插入
  54. if(avlNode.getRight() == null){
  55. AVLNode keyNode = new AVLNode(key);
  56. avlNodes.add(keyNode);
  57. avlNode.setRight(keyNode);
  58. //平衡算法
  59. calculateBFAndRotate(ancestorChain);
  60. //平衡后,画图
  61. drawStep(key);
  62. return;
  63. }else{
  64. //递归搜索
  65. insert(avlNode.getRight(),key,ancestorChain);
  66. }
  67. }
  68. }
  69. /**
  70. * 计算平衡因子,如果有必要的话,还要旋转
  71. * @param ancestorChain 祖先链
  72. */
  73. private void calculateBFAndRotate(List<Ancestor> ancestorChain){
  74. //ancestorChain持有的都是新插入的那个导致树不平衡的结点的祖先
  75. // 祖先的顺序是:index为0时,为距离最远的祖先,index为size-1时,为距离最近的祖先
  76. //所以,如果我们要找出最小不平衡子树,则需要从最近的祖先开始判断
  77. for (int i = ancestorChain.size()-1;i >= 0;i--){
  78. //当前祖先
  79. AVLNode ancestorNode = ancestorChain.get(i).getAncestor();
  80. boolean leftOfAncestor = ancestorChain.get(i).isLeft();
  81. //分别计算当前祖先的左右子树深度
  82. int leftDepth = searchDepth(ancestorNode.getLeft(),1);
  83. int rightDepth = searchDepth(ancestorNode.getRight(),1);
  84. //计算当前祖先的平衡因子
  85. int bf = leftDepth-rightDepth;
  86. //如果bf绝对值大于1,则表示树不平衡,需要进行相应的处理;否则,不做任何事情
  87. if(Math.abs(bf) > 1){
  88. //先判断当前祖先的子节点 与 当前祖先 的BF符号是否一致
  89. AVLNode child = leftOfAncestor?ancestorNode.getLeft():ancestorNode.getRight();
  90. int childLeftDepth = searchDepth(child.getLeft(),1);
  91. int childRightDepth = searchDepth(child.getRight(),1);
  92. int childBf = childLeftDepth-childRightDepth;
  93. //符号不一致,以子结点为根结点进行旋转
  94. if(childBf > 0 && bf < 0){
  95. //右旋,旋转一个单位即可,应该没有特殊情况
  96. rightRotate(ancestorNode,child,leftOfAncestor);
  97. }else if(childBf < 0 && bf > 0){
  98. //左旋
  99. leftRotate(ancestorNode,child,leftOfAncestor);
  100. }
  101. //符号统一之后,再旋转当前结点
  102. if(bf > 0){
  103. // 右旋
  104. if(i == 0){
  105. //根结点的旋转特殊处理
  106. AVLNode rootLeftRight = ancestorNode.getLeft().getRight();
  107. AVLNode rootLeft = ancestorNode.getLeft();
  108. rootLeft.setRight(ancestorNode);
  109. ancestorNode.setLeft(rootLeftRight);
  110. //更新可变根结点
  111. variableRoot = rootLeft;
  112. }else{
  113. //非根结点处理,将当前祖先的父节点,当前祖先,当前祖先与其父节点的关系传给rightRotate方法
  114. rightRotate(ancestorChain.get(i-1).getAncestor(),ancestorNode,ancestorChain.get(i-1).isLeft());
  115. }
  116. }else if(bf < 0){
  117. // 左旋
  118. if(i == 0){
  119. //根结点的旋转特殊处理
  120. AVLNode rootRightLeft = ancestorNode.getRight().getLeft();
  121. AVLNode rootRight = ancestorNode.getRight();
  122. rootRight.setLeft(ancestorNode);
  123. ancestorNode.setRight(rootRightLeft);
  124. //更新可变根结点
  125. variableRoot = rootRight;
  126. }else{
  127. //非根结点处理,将当前祖先的父节点,当前祖先,当前祖先与其父节点的关系传给rightRotate方法
  128. leftRotate(ancestorChain.get(i-1).getAncestor(),ancestorNode,ancestorChain.get(i-1).isLeft());
  129. }
  130. }
  131. }
  132. }
  133. }
  134. //左旋时,防止右节点的左孩子不为空
  135. private void leftRotate(AVLNode parent,AVLNode node,boolean isLeft){
  136. //由于是左旋,为了避免不满足二叉查找树特性,所以要先判断node的右孩子的左孩子是否为null
  137. AVLNode nodeRightLeft = node.getRight().getLeft();
  138. if(isLeft){
  139. parent.setLeft(node.getRight());
  140. parent.getLeft().setLeft(node);
  141. node.setRight(nodeRightLeft);
  142. }else{
  143. parent.setRight(node.getRight());
  144. parent.getRight().setLeft(node);
  145. node.setRight(nodeRightLeft);
  146. }
  147. }
  148. //右旋时,防止左节点的右孩子不为空
  149. private void rightRotate(AVLNode parent,AVLNode node,boolean isLeft){
  150. //由于是右旋,为了避免不满足二叉查找树特性,所以要先判断node的左孩子的右孩子是否为null
  151. AVLNode nodeLeftRight = node.getLeft().getRight();
  152. if(isLeft){
  153. parent.setLeft(node.getLeft());
  154. parent.getLeft().setRight(node);
  155. node.setLeft(nodeLeftRight);
  156. }else{
  157. parent.setRight(node.getLeft());
  158. parent.getRight().setRight(node);
  159. node.setLeft(nodeLeftRight);
  160. }
  161. }
  162. //查找树的深度
  163. private int searchDepth(AVLNode avlNode, int depth){
  164. if(avlNode == null){
  165. return 0;
  166. }
  167. if(avlNode.getLeft() == null && avlNode.getRight() == null){
  168. return depth;
  169. }else if(avlNode.getLeft() == null){
  170. return searchDepth(avlNode.getRight(),depth+1);
  171. }else if(avlNode.getRight() == null){
  172. return searchDepth(avlNode.getLeft(),depth+1);
  173. }else{
  174. int a = searchDepth(avlNode.getLeft(),depth+1);
  175. int b = searchDepth(avlNode.getRight(),depth+1);
  176. return a>b?a:b;
  177. }
  178. }
  179. /**
  180. * 平衡二叉树结点插入入口
  181. * @param root 根结点
  182. * @param keys 要插入的关键字数组
  183. */
  184. public void insertEntrance(AVLNode root,int[] keys){
  185. if(null == root){
  186. root = new AVLNode(keys[0]);
  187. variableRoot = root;
  188. this.avlNodes.add(root);
  189. for (int i = 1; i < keys.length; i++) {
  190. List<Ancestor> parentPoints = new ArrayList<>();
  191. insert(variableRoot,keys[i],parentPoints);
  192. }
  193. }else{
  194. variableRoot = root;
  195. for (int i = 0; i < keys.length; i++) {
  196. List<Ancestor> parentPoints = new ArrayList<>();
  197. insert(variableRoot,keys[i],parentPoints);
  198. }
  199. }
  200. }
  201. }

AVLMain

  1. public class AVLMain {
  2. public static void main(String[] args) {
  3. AVLInsert avlInsert = new AVLInsert();
  4. avlInsert.insertEntrance(null,new int[]{3,2,1,4,5,6,7,10,9,8});
  5. }
  6. }

运行main方法,我将每一步的结果生成了图片,供大家参考,暂时我还没测出bug来,应该没啥问题。

图片

平衡二叉树应该还挺重要的,它的算法有一定难度,不过掌握了思想之后,代码写起来就很快了。

我的二维码

觉得写得不错的小伙伴,扫码关注一下我的公众号吧,谢谢呀!

20210121085816775.jpg

发表评论

表情:
评论列表 (有 0 条评论,309人围观)

还没有评论,来说两句吧...

相关阅读