目录
前言
与红黑树结缘是在分析JDK1.8以后HashMap源码的时候,之后为了学习红黑树,又把而二叉树、二叉排序树(搜索树)复习了一遍,并促成了这个系列的文章。
关于红黑树,在经典的数据结构与算法书籍《Java数据结构和算法(第二版)》、《算法导论(第三版)》都有详细介绍。
原书是英文版,以下是中文译版的链接,有兴趣可以看看:
《算法导论(第三版)》
链接:https://pan.baidu.com/s/150mR7M0a0iN-X8bwyCTdxA
提取码:93lo
《Java数据结构和算法(第二版)》
链接:https://pan.baidu.com/s/1ZcKsRTFinMDOL62l3mQc4w
提取码:2h2r
一、二叉排序树已经够用了?
我们看到二叉排序树(搜索树)结合了有序数组和链表的优点,在查找数据时的速度和使用了二分法的有序数组一样快,而且在增、删节点的速度也和链表一样。
有序数组中插入数据太慢
用二分法在有序数组中查找指定的值,所需的时间是O(log N)。但是,想在有序数组中增加或删除一个数据,要把所有比新数据大的数据往后移一位或往前移一位,平均是要移动N/2次,时间复杂度O(N)。
在链表中查找太慢
在链表中增、删节点的操作很快,只需要改变一些引用的值就可以,时间复杂度是O(1)。但是查找指定的数据项就不那么容易了,必须从头到尾的依次比较。平均要访问N/2个数据项,时间复杂度是O(N)。
查找 | 增、删 | |
---|---|---|
有序数组 | O(log N) | O(N) |
链表 | O(N) | O(1) |
既然二叉排序树完美集成了有序数组和链表的优点有很好规避了两者的缺点,难道还不够用吗?为什么还要加入平衡算法,形成平衡二叉树(AVL树)和本文的红黑树。
时间复杂度降到O(N)
因为如果输入的数值不是随机的而是如下图所示的,此时查询速度又降到了O(N)。可能大家对O(log N)和O(N)这样的表达式没有什么概念,那我们假设有10000个数据项,对于O(logN)=14,而对于链表平均需要5000次比较!
也就是说二叉排序树只有在节点分布均匀的时候才能体现他的优势,这就促成了多种包含平衡算法的二叉排序树,红黑树是其中之一。
二、红黑树的定义
我们已经知道红黑树是加了平衡算法的二叉排序树,这个平衡算法就是红-黑规则:
(1)每一个节点要么是红色,要么是黑色;
(2)根节点是黑色;
(3)所有叶节点是黑色;
(4)如果节点是红色,他的一个或两个子节点必须是黑色;
(5)从根节点到所有叶节点的每条路径上,必须包含相同数目的黑色节点。
只要同时满足这些规则,就一定会有“红黑树可以保证任何两条从根节点到叶节点的路径节点个数相差不超过2倍”这个平衡的性质,即任何两条路径上的节点数相差不超过2倍。
看到这里一些人可能开始想了“哇,这个规则好麻烦呀,又是怎么使红黑树保持平衡的呢?我要证明一波”,打住,方向有点偏了,我们把这些规则看做数学中的定理就好,拿来用就是了,至于更深的层次,现在还没到火候。
图一就是一个红黑树。
三、怎么保持平衡
红黑树保持平衡的操作只有两个,先看容易欺负的操作一:变色,即改变节点的颜色;
操作二:旋转,包括左旋和右旋。
看到这里有些人可能会想“旋转什么的,好麻烦呀,要用程序实现不可能的吧”,不要担心,等你能在纸上画出左旋、右旋的图时,代码已经成竹在胸了。
1.左旋
很多书上都会让读者死记一些旋转规律,比如“右子节点成为父节点,右子节点的左子节点变成原父节点的右子节点,左子节点保持不变。”
其实没有必要,红黑树也是二叉排序树(搜索树),他的节点之间是有大小规律的,仔细看上图,已经标明了节点之间的大小关系,这才是为什么旋转之后要这么连接的根本原因。
2.右旋
同理,右旋也是这样的原理哦。
四、查找
红黑树的查找同理与二叉排序树。
有兴趣的可以看看二叉排序树(搜索树)查找、增加、删除操作.
五、增加
在红黑树中增加一个节点的操作分两步,第一步:查找,即要成为哪一个节点的儿子;第二步:插入,正是成为这个节点的儿子。第一步同理于查找,这里不多说,重点在于第二步。
现在我们已经找到了待插入的位置,那么我们要插入什么颜色的节点呢?
如果选黑色,发现不论父节点是红色还是黑色,这条路径上的黑色节点数相对于其他路径上都多了一个(违反红黑规则5),要进行一个整体的平衡调整。
选红色,如果很幸运父节点是黑色的话,可以直接插入,不做任何调整;如果父节点是红色,再做平衡调整。
可见插入节点是红色的话可以省点事,那就选红色嘛。
插入的所有情况
分类的方式有很多种,我们当然要参考最权威的,即JDK8源码(HashMap内部类TreeNode<K,V>的balanceInsertion()方法)的分类方式,总结如下图。
情况就这么八种,我们一种一种来分析。
1.空树
最简单的。因为默认新增的节点是红色,即变色为黑色(红-黑规则二:根节点是黑色)就行了。
2.父节点是黑色
也简单。先判断于父节点的大小,再决定是成为左儿子还是右儿子。
3.父节点是红色
3.1父节点是祖父节点的左儿子
3.1.1叔叔节点是红色
这种情况只需要通过变色处理就可以保持平衡了。
处理:
1.把xpp变成红色
2.把xp和xppr变成黑色
3.将xpp视为当前插入点x
这种情况下,就不分插入节点是父节点的左儿子还是又右儿子,处理方式是相同的,如下面两张图所示。
3.1.2叔叔节点不存在
首先我们思考有没有叔叔节点是黑色的情况呢?即这样的。
答案是没有。因为这违反了红-黑规则(5):从根节点到所有叶节点的每条路径上,必须包含相同数目的黑色节点。
同理这样的也没有。
3.1.2.1插入节点是父节点右儿子
处理:
对xp做左旋操作
右旋之后就是下面3.1.2.2的情况,同样处理就可以了。
3.1.2.2插入节点是父节点左儿子
我们还是标上一些数值方便看是怎么旋转的。
处理:
1.xpp变成红色
2.xp变成黑色
3.对xpp进行右旋操作
3.2父节点是祖父节点的右儿子
3.2.1叔叔节点是红色
同理与3.1.1的情况,只需要做变色处理就可以保持平衡了。
处理:
1.把xpp变成红色
2.把xppl和xp变成黑色
3.将xpp视为当前插入点x
3.2.2叔叔节点不存在
3.2.2.1插入节点是父节点的左儿子
处理:
对xp做右旋操作
右旋之后就是下面3.2.2.2的情况,同样处理就可以了。
3.2.2.2插入节点是父节点的右儿子
我们还是标上一些数值方便看是怎么旋转的。
处理:
1.xpp变成红色
2.xp变成黑色
3.对xpp进行左旋操作
六、红黑树的时间复杂度
查找 | 增 | 删 | |
---|---|---|---|
红黑树 | O(log N) | O(log N) | O(log N) |
查找
因为“红黑树可以保证任何两条从根节点到叶节点的路径节点个数相差不超过2倍”这个平衡的性质。所以红黑树查找的时间复杂度大约是O(log N) ,最差也不会超过2*O(log N)。
附录
1.这里附上HashMap内部类TreeNode<K,V>的balanceInsertion()方法。
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
x.red = true;
//这里的x、xp、xpp、xppl、xppr就是上面图中的节点
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//1.空树
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//2.父节点黑色或祖父节点为空
else if (!xp.red || (xpp = xp.parent) == null)
return root;
//3.父节点红色
//3.1父节点是祖父节点的左儿子
if (xp == (xppl = xpp.left)) {
//3.1.1叔叔节点红色
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
//3.1.2叔叔节点不存在
else {
//3.1.2.1插入节点是父节点右儿子
if (x == xp.right) {
//对xp左旋
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//3.1.2.2插入节点是父节点左儿子
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
//对xpp右旋
root = rotateRight(root, xpp);
}
}
}
}
//3.2父节点是祖父节点右儿子
else {
//3.2.1叔叔节点红色
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
//3.2.2叔叔节点不存在
else {
//3.2.2.1插入节点是父节点左儿子
if (x == xp.left) {
//对xp右旋
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//3.2.2.2插入节点是父节点右儿子
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
//对xpp左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
2.这是这个系列的前两篇文章,有兴趣的可以看看。
第一篇:二叉树(从建树、遍历到存储)Java.
第二篇:二叉排序树(搜索树)查找、增加、删除操作.