数据结构,java实现平衡二叉树,AVL树

本文深入探讨了平衡二叉树的原理及其在Java中的实现,重点讲解了树的平衡性维护,包括左旋、右旋操作及平衡因子的计算,同时提供了插入和删除节点的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前看HashMap源码的时候遇到一些问题,jdk1.8中对HashMap做了一些优化,其中就包括当链表节点大于8个时会将链表转换成红黑树以提高查找效率。我对红黑树了解的比较少,而且之前学的二叉树相关的知识也忘了许多,所以决定近期去复习一下。

红黑树是一种平衡二叉树,平衡二叉树又属于二叉搜索树(二叉排序树)。这几天我就从二叉搜索树开始,把这几种数据结构都用java实现了一遍,二叉排序树比较简单没什么可以总结的,所以这篇文章就主要讲一下平衡二叉树的原理和实现吧,等以后忘了的时候还可以回来看。下一篇在讲红黑树,因为红黑树我还有些问题没搞懂。

先回顾一下二叉排序树的基本知识,在二叉排序树中,每个节点的左边的节点都比该节点小,右边的节点都比该节点大。它可以比较快的完成查询操作,但是却存在一个弊端。那就是当插入时如果插入的节点的顺序是基本有序的,那么形成的二叉搜索树就是非常不平衡的,最极端的情况会直接退化成为链表,这样会大大降低查询效率。

平衡二叉树就是为了解决这个问题而出现的,它可以保证无论以什么样的顺序插入节点,最后生成的树的左右子树高度之差的绝对值(平衡因子)不会超过1。因为每次向树中插入一个节点之后都会计算平衡因子(左右子树高度差),平衡因子大于1则视为不平衡,需要通过左旋或者右旋调整平衡。

插入

插入时大概可以分为以下几种情况(不需要调整平衡的情况就不说了):

一:只需进行一次旋转操作就可以调整平衡,可能是左旋也可能是右旋,这里只说左旋的情况,右旋对称过去就可以了。

/**
 * 左旋
 *
 * 第一种情况
 *
 *      1
 *       \                     2
 *        2        --->      /  \
 *         \                1    3
 *          3
 *
 * 第二种情况
 *
 *     2
 *    / \                    4
 *   1   4                 /  \
 *      / \     --->      2    5
 *     3   5            /  \    \
 *          \          1    3    6
 *           6
 *
 *
 * 
 */

如图中第一种情况所示,3 插入之后导致该树不平衡。要调整平衡只需要从新插入的 节点3 开始向上查找,找到最小不平衡子树(从下往上第一个高度差绝对值大于1的子树),对最小不平衡子树进行旋转即可。向上查找发现,节点1 的平衡因子是 -2 所以节点一就是最小不平衡子树的树根,并且 节点1 的右子节点(也就是 节点2 )的平衡因子是 -1,同为负数。所以只需要进行一次左旋即可。

什么是左旋?怎么左旋?

根据上面的例子,左旋大概可以理解为对 节点1 进行逆时针旋转,让 节点1 的右子节点做父节点,节点1 做左子节点。也就是:右子变新父,原父变左子。但也存在特殊情况,在情况一中,节点2 变成新的父节点之后,它的左右子节点都不为空,那么如果 节点2 原来就有左子节点该怎么办?请看情况二。

在第二种情况中,节点6 的插入导致了该树的不平衡,通过分析(节点2平衡因子为-2,节点4平衡因子为-1)可以得出需要对 节点2 进行左旋。这里有一个很尴尬的问题:节点4 原本就存在一个左子节点(节点3),如果把节点4变成新的父节点之后,节点3该放哪?我第一次看到这就很懵逼,当时看的是一个动图,盯着看了好多遍,眼都要花了,气的想把多出来的节点拔下来,好像扯远了。。。。。。回到正题,通过观察可以发现,节点3 是最小不平衡子树的右子树中的最小的一个节点。它比原父节点要大,比新的父节点要小,那么就可以放到原父节点的右边,因为原父节点的右子节点不是变成了新父节点了嘛,所以就刚好空出来了。总结一下也就是,右子的左子变左子的右子,好像有点绕口。。。左旋的就是这样,右旋跟左旋是对称的所以没必要讲。

 

二:需要先左旋再右旋或者先右旋在左旋才能调整平衡。

这种情况的基本形态是这样的:

/**
 *
 * 先右旋再左旋
 * 
 *   1             1
 *    \             \                 2
 *     3   --->      2     --->      / \
 *    /               \             1   3
 *   2                 3
 *   
 *   
 *   
 */

按照上面的方法,从节点2开始向上找最小不平衡子树,通过计算得出,节点1 的平衡因子是 -2,节点3 的平衡因子是 1 。一正一负,直接左旋是不行的,不信你可以试试。这里就需要先对 节点3 进行右旋,在对节点1进行左旋,跟只旋转一次也没多大区别,就不讲了。

怎么判断是进行左旋还是右旋?

这的看是怎么计算平衡因子的,我是用左子树高度减右子树高度的方法。所以平衡因子为负数,那么右边比较高,就需要进行左旋,为正数就是左边比较高,需要进行右旋。

代码实现

/**
 * 计算插入位置并插入节点
 * @param e    节点中的数据
 * @return 返回被插入的节点,用来从下往上检查平衡性
 */
private Node addNode(E e) {
   Node node = root;
   // 树为空时直接插入根节点
   if(node == null) {
      root = new Node(e);
      size = 1;
      return root;
   }
   while(node != null) {
      int result = e.compareTo(node.data);
      if(result < 0) {
         if(node.left != null) {
            node = node.left;
         }else {
            size++;
            node.left = new Node(node, e);
            node = node.left;
            break;
         }
      }else if(result > 0) {
         if(node.right != null) {
            node = node.right;
         }else {
            size++;
            node.right = new Node(node, e);
            node = node.right;
            break;
         }
         // 插入节点已存在时不做插入操作,但是 modCount 会自增
      }else {
         break;
      }
   }
   return node;
}
​
/**
 * 从node节点开始 向上查找失去平衡的节点,得到最小不平衡子树的树根,并调整平衡
 * @param node 开始查找的节点(新插入的节点)
 */
private void checkAndAdjustBalance(Node node) {
   // 平衡因子
   int balanceFactor = 0;
   // 找到最小不平衡子树
   while(node.parent != null) {
      node = node.parent;
      balanceFactor = calculateBalanceFactor(node);
      if(balanceFactor == -2 || balanceFactor == 2) {
         break;
      }
   }
   // 调整平衡
   if(balanceFactor == 2 || balanceFactor == -2) {
      // 左旋或者右旋维持平衡
      if(balanceFactor == 2) {
         if(calculateBalanceFactor(node.left) == -1) {
            // 先左旋
            leftRotate(node.left);
         }
         // 右旋
         rightRotate(node);
      }else {
         if(calculateBalanceFactor(node.right) == 1) {
            // 先右旋
            rightRotate(node.right);
         }
         // 左旋
         leftRotate(node);
      }
   }
}
​
/**
 * 计算该树的平衡因子,平衡因子为 左子树深度 减 右子树深度
 * @param root 需要计算平衡因子的树的树根
 * @return 平衡因子
 */
private int calculateBalanceFactor(Node root) {
   int leftDepth = depth(root.left);
   int rightDepth = depth(root.right);
   return leftDepth - rightDepth;
}
​
/**
 * 左旋
 * @param n 最小不平衡子树的树根
 */
private void leftRotate(Node n) {
   // 先把右子的parent指针弄好。
   n.right.parent = n.parent;
   // 判断原父是否为root,不是的话把原父的父节点,和新父连接。
   if(n.parent != null) {
      if(n.parent.left == n) {
         n.parent.left = n.right;
      }else {
         n.parent.right = n.right;
      }
   }else {
      this.root = n.right;
   }
   // 将原父的父节点指针指向新父
   n.parent = n.right;
   // 将新父的原左子,变成原父的右子
   n.right = n.parent.left;
   if(n.right != null) {
      n.right.parent = n;
   }
   // 新父的左子节点指针指向原父
   n.parent.left = n;
}
​
/**
 * 右旋
 * @param n
 */
private void rightRotate(Node n) {
   // 先把左子的parent指针弄好
   n.left.parent = n.parent;
   // 判断原父是否为root,不是的话把原父的父节点,和新父连接。
   if(n.parent != null) {
      if(n.parent.left == n) {
         n.parent.left = n.left;
      }else {
         n.parent.right = n.left;
      }
   }else {
      this.root = n.left;
   }
   // 将原父的父节点指针指向新父
   n.parent = n.left;
   // 将新父的原右子,变成原父的左子
   n.left = n.parent.right;
   if(n.left != null) {
      n.left.parent = n;
   }
   // 新父的左子节点指针指向原父
   n.parent.right = n;
}

删除

平衡二叉树的删除跟二叉排序树差不多,基本可以分为以下三种情况:

1.被删除节点存在两个子节点。

2.被删除节点存在一个子节点。 3.被删除节点不存在子节点。

对于第三种情况,可以直接删除,只需要把相关的指针“断开”就好。

对于前两种,我们可以把被删除节点跟一个叶子节点互换,然后直接删除。互换删除的同时还要保证树是平衡的,所以不能随便选择叶子结点互换。对于第一种情况,可以与左子树中的最大节点互换,也可以与右子树中的最小的节点互换。要注意的是,被交换的节点不一定是叶子节点,可能存在0-1个子节点,当存在子节点时就当做第二种情况处理。

第二种情况就直接跟它的子节点互换就好,因为是平衡二叉树,平衡因子不能大于一,所以当该节点存在一个子节点的时候,那么该节点一定没有孙子节点,也就是说它的那一个子节点一定是叶子结点,可以直接互换。

删除之后别忘了重新验证平衡,因为少了一个节点导致此节点所在的子树的高度减一,可能会出现不平衡的情况。说多了也没用,还是直接上代码吧。

 

代码实现

/**
 * 删除node节点,要分别处理 node节点有 0,1,2 个子节点的情况
 * @param node
 */
private void removeNode(Node node) {
   modCount++;
   size--;
   // 当被删除节点存在两个子节点时,那被删除节点和右子树中最小节点互换,或者和左子树中最大节点互换
   if(node.left != null && node.right != null) {
      Node minNode = minNode(node.right);
      node.data = minNode.data;
      node = minNode;
   }
   Node childNode = node.left != null ? node.left : node.right;
   // 当被删除节点存在一个子节点时
   if(childNode != null) {
      node.data = childNode.data;
      node = childNode;
   }
   if(node == node.parent.left) {
      node.parent.left = null;
   }else {
      node.parent.right = null;
   }
   checkAndAdjustBalance(node);
}

本人水平有限,代码可能存在一些错误,欢迎大佬指正。要说的就这么多了,红黑树的过几天在整理吧,最后附上这个java项目github地址

https://github.com/Numblgw/BinaryTreeDemo

原创文章,转载请注明作者和出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值