前言
前面我们提到,为了解决二分搜索树有时候会退化成链表的问题,科学家们提出了平衡树的概念,最早发明的AVL树就是高度平衡(所有节点的左右子节点的高度差的绝对值不超过1)的二分搜索树,但实际开发中,AVL树并不常见,工程中,常用的平衡二分搜素树是这篇我们要介绍的红黑树。那到底什么是红黑树呢?我们在这篇文章中揭晓。为了便于理解红黑树,我们先来看一种绝对的平衡树–2-3树。
所谓绝对平衡树就是所有节点的左右子节点的高度差都为0,顾名思义,2-3树就是有的节点可以存放一个或两个元素,且可以有2个子节点,也可以有3个子节点。2-3树任然满足二分搜索树的基本性质,即每个节点的左子树小于该节点的值,每个节点的右子树大于该节点的值,不同的地方在于,当2-3树的节点有3个子节点的时候,那么父节点有两个元素,需要考虑子节点位于父节点两个元素之间的情况。具体的2-3树如图所示:
那么是一棵绝对的平衡树,那么在添加元素时,2-3树是如何维持这种绝对的平衡性的呢?接下来结合图和分析:
如上图,刚开始2-3树中是没有元素的,当我们添加42的时候,直接添加到根节点的位置即可。接着添加37,37比42小,应该添加到42的左子树上,但是42的左子树为空,而2-3树添加元素的时候不能把元素添加在空的位置上,所以要把37先融合到添加元素过程中找到的最后一个叶子节点上,又当前只有42,42即为叶子节点,所以37添加到42这个节点上,融合成一个3节点(可以有三个子节点)。接着再添加12到2-3树中,12比37小,应该添加到37左子树,但37的左子树为空,所以先把12融合到37节点上,形成一个4节点(可以有四个子节点),但对于2-3树来说,不能有4个子节点,最多可以有3个子节点,所以再把当前的这个四节点分裂为一棵子树,将一个4节点转换为由3个2节点组成的子树。
在上述的基础上,再添加18这个节点,比较后,18应添加在12的右子树,但12的右子树为空,所以18融合到12这个节点。接着再添加6,6应该添加在包含12和18两个元素的3节点的左子树,但左子树为空,所以融合到12这个节点上。接着需要对融合后的这个4节点拆解,拆解后的树不满足绝对平衡性,还需要将拆解后的子树的新的父节点向上和它的父节点融合,12的父节点是根节点37,所以12和37这个节点融合,根节点变为一个3节点,12原来的左节点6成了包含12和37两个元素的3节点的左子节点,12原来的右节点18则成了这个3节点的中间节点,此时满足2-3树的绝对平衡性。
接着再添加11这个节点,11应该添加6的右子树,右子树为空,11和6融合。再添加5这个节点,5与包含6和11两个元素的这个3节点融合,形成一个4节点,将这个4节点分裂,分裂后不满足绝对平衡性,再向上融合,融合后根节点又变为4节点,再分裂,之后满足绝对平衡性。
1.什么是红黑树
红黑树没有一个统一的定义,我们学习数据结构是为了实际中应用,所以没必要死扣定义。我们这里这样来定义红黑树:每个节点只存储一个元素实现和2-3树一样的逻辑的树就是红黑树,2-3树中2节点只存储一个元素,可以有两个子节点,红黑树中也用这样的节点来表示,2-3树中的3节点存储两个元素,可以有三个子节点,红黑树用两个节点来表示,如图,b节点和它的父c一起来表示2-3树中包含b和c这两个元素的三节点。在红黑树中所有的红色节点都是向左倾斜的,因为红色节点表示2-3树中一个三节点左边的元素。
其实红黑树和2-3树具有等价关系,下图列出红黑树和2-3树的这种等价关系:
2.红黑树的性质
- 每个节点要么是红色的,要么是黑色的。
- 根节点时黑色的。
- 每一个叶子节点(最后的空节点)是黑色的。
- 如果一个节点时红色的,那么它的孩子节点都是黑色的。
- 从任意一个节点到叶子节点,经过的黑色节点数量是一样的。
红黑树的黑色节点高度差不超过1,是保持“黑平衡的二叉树”,所以,红黑树不是严格意义上的平衡二叉树。
3.红黑树的添加操作
红黑树美添加一个节点默认都为红色,这是因为红黑树和2-3树具有等价关系,而2-3树是不能把一个新的节点添加到元素为空的位置的,每次为2-3树添加元素都是将要添加的节点融合到已有的节点上,又红黑树中用红色节点来表示那个融合的节点,所以红黑树新添加的节点都为红色。当红黑树为空的时候,添加的第一个节点作为根节点,又红黑树的根节点是黑色的,因此,需要在添加节点时将根节点最终设置为黑色(root.color = BLACK )。
- 左旋转:当红黑树要插入的节点比当前的节点大时,需要插入到该节点的右子树的位置,而红黑树的红色节点都应插入到当前节点的左子树的位置,因而需要将当前左旋转,使红色节点插入到左边的位置。
为了不失一般性,我们假设13和27都有子节点,此时的左旋图如下:
代码实现:
private Node leftRotate(Node node){
Node x = node.right;
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return node;
}
- 颜色翻转:如图,要添加的节点66比42大,应该添加在42的右侧,此时有42有两个红色的子节点,对应于2-3树中的一个四节点。在2-3树中一个四节点会分裂为三个二节点,所以将红黑树中对应的37,16这两个红色的节点全部变为黑色即可,不要要旋转,又2-3树中分裂后父节点还要向上融合,因此需要把红黑树中的父节点42变为红色。
代码实现:
private void flipColors(Node node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
- 右旋转:要添加的节点12比37小,应添加到37的左侧,此时对应于2-3树中的一个四节点,这个四节点需要分裂为以37为父节点的3个二节点,对应的红黑树中42则需要右旋转。旋转后将37的颜色变为黑色,因为37为父节点,42变为红色,此时红黑树的结构就和上面颜色翻转前情况一样了,接着进行颜色翻转即可。
代码实现:
private Node rightRotate(Node node){
Node x = node.left;
//右旋转
x.right = node;
node.left = x.right;
x.color = node.color;
node.color = RED;
return x;
}
如果要添加的元素正好处于两个节点之间,这时就需要先左旋转,再右旋转,然后进行颜色翻转。如下图(先37左旋转,再42右旋转,接着颜色翻转):
添加代码实现:
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value);
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
if(isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
if(isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
if(isRed(node.left) && isRed(node.right))
flipColors(node);
return node;
}
4.总结
这篇主要介绍了红黑树的概念,性质以及红黑树和2-3树的等价关系,还介绍了红黑树的添加操作,红黑树的主要优势相较于二分搜索树和AVL树就在于添加操作,红黑树的高度最高可以达到2logn的高度,所以红黑树在查询方面并不占优,但红黑树是一种统计性能更优的树结构。其实关于红黑树的添加操作还有可优化的地方,就不在这里详细介绍了,下篇将红黑树的代码实现贴出来。