一、概念理解
红黑树是一种自平衡二叉查找树,在插入或者删除节点时,都有特定的操作,保持二叉查找树的平衡,用来获得比较好的查找性能。
二、红黑树的特性
1、节点不是黑色就是红色;
2、根节点是黑色;
3、每个红色节点的两个叶子节点都是黑色;
4、任何一个节点到其叶子节点的所有路径上的黑色节点数相同。
三、对特性的解释
-
首先,为什么要设置颜色?
颜色可以看成一种特定操作的依据,用以制约二叉查找树平衡。
对于第三点,也就是说任何一条从根节点到叶子节点的路径上不可能有两个毗邻的红色节点。
-
那我们想一想从根节点到任一叶子节点的所有路径中最短的是哪一条?
是不是全是黑色节点组成的,是,当然是
-
那最长路径呢?
到叶子节点黑红相间的那条高度最高的路径
然后我们再看看第四条限制,根到任一叶子节点的路径上的黑色节点数相同!!!把这点结合起来看我们前边说的最短路径和最长路径,是不是可以得出一个结论-------最长路径最多是最短路径的两倍。
于是,有了这一结论,基本可以保证二叉查找树的相对平衡,查找效率也基本上可以保证。
四、我们看看变色和旋转的规律
新插入节点时,因为要插入到某个叶子节点的左子节点或者右子节点,所以其颜色肯定是红色的,如果是黑色那必然违反第四条(本来树的每个路径上的黑色节点数目相同)。
所以我们可以不管新插入的节点,我们要分析其父节点
当新插入的节点的父节点为红色时,必须改变父节点的颜色为黑色。
旋转规律:
当前节点值大于其父节点值,左旋,因为左旋后父节点变成当前节点的左子节点,符合左子节点值小于父节点的条件。
当前节点值小于其父节点值,右旋,因为右旋后父节点变成当前节点的右子节点,符合右子节点值大于父节点的条件。
所以,如果要旋转,必然是这种操作。
五、平衡调节详解
平衡调节的一个单元在当前节点、父节点、祖父节点这三代之间进行,整棵红黑树由小单元组成,所以我们只需研究透这一单元,整棵树也就不是问题了。
我们不考虑既不用改变颜色,也不用旋转的情况,也就是插入节点的父节点为黑色的情况。此外,就剩以下几种情况。
第一种情况:先看图1
图中18为新插入数据节点,其父节点为10,根节点为20。就此图中的场景,要做怎样的变换使树符合红黑树的特性呢?
先尝试变颜色,我们发现将10和25所在节点颜色变为黑色,即可符合红黑树特性。如下
如果18为10的左子节点,情况一致。
第二种情况:
图中18为新插入数据节点,其父节点为10,根节点为20,和第一种情况的区别是少了和10同级的25节点。那我们再看看这个怎么变化?
很容易看出,单改变颜色是没法使其符合红黑树特性的。
因为18大于10(是10这个节点的右子节点),所以我们根据上述总结的旋转规律,进行左旋,使10成为18的左子节点,变成如下图
但我们发现经过这一次旋转仍然没有达到目的,而且跟没旋转之前,本质上没变多少。但这是必要的,因为单改变颜色解决不了问题,那么我们就依据旋转规律的条件来进行旋转,即使一次旋转不能解决问题,那也为后续的旋转等操作够造了符合旋转规律的条件。
此时,我们看到18小于20,根据旋转规律,我们进行右旋,使18成为根节点并变为黑色,20成为18的右子节点。如下图
此时,我们发现,已经符合红黑树特性定义。
第三种情况:
我们要构造出在第一种情况中(图1),节点25所在节点(也就是与父节点同级的那个节点)颜色为黑色的情况。这样便覆盖了我们这个小单元的所有情况。
但这个不能用三个节点直接构造出来,那我用经过一次新加节点做变换的方式来构造这样的效果,先构造一棵红黑树如下
基于这棵树,我们暂且只关注蓝框内的这个单元,我们再添加一个节点12,正好符合情况1,只需要做情况一中的改变,这个单元即符合红黑树定义。
但是,这个单元作为整棵树的一部分,显然做了情况一种的变换之后,蓝框所在各分支便比整棵树中其他各分支多了一个黑色节点。所以,做情况一的变换时,如果小单元的根节点不是整棵树的根节点时,需要把小单元的“根”节点置为红色。那这棵树就变成如下状态
变换到这里,便出现了我们需要的第三种情况,然后我们考虑这一单元
目前各分支,黑色节点的数目均是相等的,只需使其符合红黑树特征3即可,明显可以看出,更改颜色是肯定不行的。
所以我们根据旋转规律,进行一次左旋,得到如下图的状况
我们看10、18、20三个节点是不是很符合第二种情况的最后一步?
那我们就做一次和第二种情况最后那一步相同的变换,以18节点为当前节点进行一次右旋,18节点变为根节点(染黑色),20节点变为18节点的右子节点(染红色)。如下
到此,已经是一棵完整的红黑树了。
于是,我们可以对第三种情况做一个总结:也就是说,在插入新节点时,如果其父节点的兄弟节点颜色和其父节点的颜色不同,那么进行红黑树修正所进行的操作和其父节点的兄弟节点为空时所做的修正操作相同。
六、以红黑树插入新节点为例详解代码
基于以上的分析基础,我们来深入理解红黑树平衡过程代码,我们不另外造轮子,就参考HashpMap中的红黑树插入操作代码。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
// 新插入节点做红色标记
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
// 如果插入节点为根节点(初始插入节点),那么染黑节点直接返回该节点
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果插入节点的父节点为黑色节点,不用调整,直接返回根节点
// 如果插入节点的祖父节点为空,说明插入节点其父节点即为根节点,根节点是黑色,不用调整,直接返回根节点
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 以下即是插入新节点,做红黑树特性调整前新节点所在分支的高度至少为3了
// 此分支是作为左子树的情况,左右具有对称性,所以这里只分析这一分支
if (xp == (xppl = xpp.left)) {
// 这一情况便是我们上边分析过的,插入节点的父节点的兄弟节点为红色
// 也就是插入节点的父节点和其父节点的兄弟节点同色,均为红色
// 根据我们上边分析的情况(见情况一),只需调整颜色即可
if ((xppr = xpp.right) != null && xppr.red) {
// 具体是将父节点和父节点的兄弟节点均改为黑色
xppr.red = false;
xp.red = false;
// 其祖父节点改为红色,并将其祖父节点赋为当前节点
// 这样赋值的原因是,要做后续调整,如果下一个循环中发现这一节点已经是根节点,那么将其染成黑色
xpp.red = true;
x = xpp;
}
// else分支即是上述分析的情况二和情况三,前边已经说明过,其调整方式相同
else {
// 可以把之前总结的旋转规律套用在这里
// 因为当前节点是父节点的右子节点,其值比父节点大,所以进行左旋
// 旋转操作过程中将当前节点x及xp及xpp重新赋值,继续遍历
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
// 这里说明一下,无论是否经过 if (x == xp.right) { 这一分支的操作,此时情况已经变
// 成图4状况了,此时使用用右旋即可,同时将其父节点染黑,将其祖父节点染红即可
root = rotateRight(root, xpp);
}
}
}
}
else {
// 这一部分和if分支具有对称性
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}