之前看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
原创文章,转载请注明作者和出处。