数据结构之红黑树Java实现
【本文是为了梳理知识的总结性文章,总结了一些自认为相关的重要知识点,只为巩固记忆以及技术交流,忘批评指正。其中参考了很多前辈的文章,包括图片也是引用,如有冒犯,侵删。】
1 红黑树的进化历程
1.1 第一阶段:树
树是一种常用的数据结构,它是一个由有限节点组成的一个具有层次关系的集合,数据就存在树的这些节点中。最顶层只有一个节点,称为根节点。在分支处有一个节点,指向多个方向,如果某节点下方没有任何分叉的话,就是叶子节点。从某节点出发,到叶子节点为止,最长简单路径上边的条数,称为该节点的高度;从根节点出发,到某节点边的条数,称为该节点的深度。
树结构的特点如下:
(1)一个节点,即只有根节点,也可以是一棵树。
(2)其中任何一个节点与下面所有节点构成的树称为子树。
(3)根节点没有父节点,而叶子节点没有子节点。
(4)除根节点外,任何节点有且仅有一个父节点。
(5)任何节点可以有0 ~ n个子节点。
1.2 第二阶段:二叉树
二叉树是指至多有两个子节点的树。二叉树具有以下几个性质:
- 二叉树中,第 i 层最多有 2 ^ (i-1) 个结点。
- 如果二叉树的深度为 K,那么此二叉树最多有 2^k - 1 个结点。
- 二叉树中,终端结点数(叶子结点数)为 n0,度为 2 的结点数为 n2,则 n0=n2+1。
二叉树,本质上,是对链表和数组的一个折中,数组的搜索比较方便,可以直接用下标,但删除或者插入某些元素就比较麻烦。 链表与之相反,删除和插入元素很快,但查找很慢。 结构良好的二叉就既有链表的好处,也有数组的好处。
二分法是经典的问题拆解算法,二叉树是近似于二分法的一种数据结构实现,二叉树是高效算法实现的载体,在整个数据结构领域具有举足轻重的地位。在二叉树的世界中,最为重要的概念是平衡二叉树、二叉查找树、红黑树。
1.3 平衡二叉树
下图中左右两个都是树,但是左侧这颗树已经退化为了链表,有建立树与没建立树对于数据的增删查改已经没有了任何帮助,反而增添了维护的成本。而右侧是一棵平衡二叉树,能够最大地体现二叉树的优点。
平衡二叉树的性质如下:
- 树的左右高度差不能超过1。
- 任何往下递归的左子树与右子树,必须符合第一条性质。
- 没有任何节点的空树或只有根节点的树也是平衡二叉树。
1.4 二叉查找树
二叉查找树又称二叉搜索树,即Binary Search Tree,其中Search也可以替换为Sort,所以也称为二叉排序树。二叉查找树额外增加了如下要求:对于任意节点来说,它的左子树上所有节点的值都小于它,而它的右子树上所有节点的值都大于它。二叉查找树非常适合数据查找。
二又查找树由于随着数据不断地增加或删除容易失衡,需要保持二叉树的平衡性,有很多算法的实现,如AVL树、红黑树、SBT(Size Balanced Tree)、Treap(树堆)等。
1.5 AVL 树
AVL树算法是以苏联数学家Adelson-Velsky和Landis名字命名的平衡二叉树算法,可以使二叉树的使用效率最大化。AVL树是一种平衡二叉查找树,增加和删除节点后通过树形旋转重新达到平衡。右旋是以某个节点为中心,将它沉入当前右子节点的位置,而让当前的左子节点作为新树的根节点,也称为顺时针旋转;同理,左旋是以某个节点为中心,将它沉入当前左子节点的位置,而让当前右子节点作为新树的根节点,也称为逆时针旋转。查找、插入和删除在平均和最坏情况下的时间复杂度都是。增加和删除元素的操作则可能需要借由一次或多次旋转,以实现树的重新平衡。
2 红黑树
红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",1978年得到优化,正式命名为红黑树。它的主要特征是在每个节点上增加一个属性来表示节点的颜色,可以是红色,也可以是黑色。红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中高效:时间内完成查找,插入和删除,这里的n是树中元素的数目。
红黑树和AVL树类似,都是在进行插入和删除元素时,通过特定的旋转来保持自身平衡的,从而获得较高的查找性能。与AVL树相比,红黑树并不追求所有递归子树的高度差不超过1,而是保证从根节点到叶子节点的最长路径不超过最短路径的2倍,所以它的最坏运行时间也是。红黑树通过重新着色和左右旋转,更加高效地完成了插入和删除操作后的自平衡调整。当然,红黑树在本质上还是二叉查找树,它额外引入了5个约束条件:
- 节点只能是红色或黑色。
- 根节点必须是黑色。
- 所有NIL节点都是黑色的。NIL,即叶子节点下挂的两个虚节点。
- 一条路径上不能出现相邻的两个红色节点。
- 在任何递归子树内,根节点到叶子节点的所有路径上包含相同数目的黑色节点。
总结一下,即“有红必有黑,红红不相连”。如果一个树的左子节点或右子节点不存在,则均认定为黑色。红黑树的任何旋转在3次之内均可完成。
3 红黑树的基本操作
下面安装TreeMap的红黑树实现进行介绍:
左旋
private void rotateLeft(Entry<K,V> p) {
// 如果 p 不是NULL节点
if (p != null) {
Entry<K,V> r = p.right;
// 将r的左子树设置为p的右子树
p.right = r.left;
// 如果r的左子树不为空,则将p设置为r左子树的父亲
if (r.left != null)
r.left.parent = p;
// 将p的父亲设置为r的父亲
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
右旋
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
l.right = p;
p.parent = l;
}
}
查找
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left; // 当前元素大于目标元素,向左找
else if (cmp > 0)
p = p.right; // 当前元素小于目标元素,向右找
else
return p;
}
return null;
}
插入
插入新节点后,可能会导致树不满足红黑树性质,这时就需要对树进行旋转操作和重新着色进行修复,使得它符合红黑树的定义。在插入新节点前,需要明确三个前提条件:
- 新插入的节点总是红色的;
- 如果新节点的父节点是黑色,则不需要进行修复;
- 如果新节点的父节点是红色,因为红黑树规定不能出现相邻的两个红色节点,需要进行修复。
插入修复时可能碰到的情况可分为的三种:
- 叔叔节点也为红色。
- 叔叔节点为空,且爷爷节点、父节点和新节点处于一条斜线上。
- 叔叔节点为空,且爷爷节点、父节点和新节点不处于一条斜线上。
每一种情况根据新节点的父节点是爷爷节点的左侧还是右侧可以分为两种情况,所以可以细分为6种情况。
第一种情况处理方法:
因为新节点、父节点、叔叔节点都是红色,爷爷节点是黑色,因此通过局部颜色调整,就可以使子树继续满足红黑树的性质。将父节点和叔叔节点置为黑色,爷爷节点置为红色。然后再以爷爷节点为新节点进行下一轮调整,直到满足红黑树性质为止。
第二种情况处理方法:
将新节点的父节点置为黑色,爷爷节点置为红色。如果父节点在爷爷节点左侧,则对父节点进行右旋操作。如果父节点在爷爷节点右侧,则对父节点进行左旋操作。
第三种情况处理方法:
将不在一条直线上的节点调整为直线,如果父节点在爷爷节点左侧,则对父节点进行左旋操作。如果父节点在爷爷节点右侧,则对父节点进行右旋操作。变成第二种情况,然后再进行旋转操作。
代码
private void fixAfterInsertion(Entry<K,V> x) {
// 新节点为红色
x.color = RED;
while (x != null && x != root && x.parent.color == RED) { // 循环调整
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 第一种情况,父节点在爷爷节点左侧
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
// 第三种情况,父节点在爷爷节点左侧
x = parentOf(x);
rotateLeft(x);
}
// 第二种情况,父节点在爷爷节点左侧
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 第一种情况,父节点在爷爷节点右侧
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
// 第三种情况,父节点在爷爷节点右侧
x = parentOf(x);
rotateRight(x);
}
// 第二种情况,父节点在爷爷节点右侧
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 根节点为黑色
root.color = BLACK;
}
删除
因为删除RED节点不会破坏红黑树的任何约束,而删除BLACK节点会破坏规则4,因此只有删除节点是BLACK的时候,才会触发调整函数。删除修复操作是针对删除黑色节点才有的。
删除BLACK节点后的修复操作分为四种情况:
- 待删除的节点的兄弟节点是红色的节点。
- 待删除的节点的兄弟节点是黑色的节点,且兄弟节点的子节点都是黑色的。
- 待调整的节点的兄弟节点是黑色的节点,且兄弟节点的左子节点是红色的,右节点是黑色的(兄弟节点在右边),如果兄弟节点在左边的话,就是兄弟节点的右子节点是红色的,左节点是黑色的。
- 待调整的节点的兄弟节点是黑色的节点,且右子节点是是红色的(兄弟节点在右边),如果兄弟节点在左边,则就是对应的就是左节点是红色的。
每一种情况根据删除节点是父节点的左侧还是右侧可以分为两种情况,所以可以细分为8种情况。
第一种情况处理方法:
由于兄弟节点是红色节点的时候,无法借调黑节点,所以需要将兄弟节点提升到父节点。因为兄弟节点是红色的,无法借到一个黑节点来填补删除的黑节点。兄弟节点是红色,其子节点是黑色的,可以从它的子节点借调。提升操作需要对兄弟节点做一个左旋操作,如果是镜像结构的树只需要做对应的右旋操作即可。
第二种情况处理方法:
由于兄弟节点以及其子节点都是黑色的,可以消除一个黑色节点,这样就可以保证树的局部的颜色符合定义了。这个时候需要将父节点变成新的节点,继续向上调整,直到整颗树的颜色符合红黑树的定义为止。这种情况下之所以要将兄弟节点变红,是因为如果把兄弟节点借调过来,会导致兄弟的结构不符合红黑树的定义,这样的情况下只能是将兄弟节点也变成红色来达到颜色的平衡。当将兄弟节点也变红之后,达到了局部的平衡了,但是对于祖父节点来说是不符合定义4的。这样就需要回溯到父节点,接着进行修复操作。
第三种情况处理方法:
第三种情况是一个中间步骤,它的目的是将左边的红色节点借调过来,这样就可以转换成第四种情况进行处理。
第四种情况处理方法:
是真正的节点借调操作,通过将兄弟节点以及兄弟节点的右节点借调过来,并将兄弟节点的右子节点变成红色来达到借调两个黑节点的目的,这样的话,整棵树还是符合RBTree的定义的。这种情况的发生只有在待删除的节点的兄弟节点为黑,且子节点不全部为黑,才有可能借调到两个节点来做黑节点使用,从而保持整棵树都符合红黑树的定义。
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK); // 情况1
setColor(parentOf(x), RED); // 情况1
rotateLeft(parentOf(x)); // 情况1
sib = rightOf(parentOf(x)); // 情况1
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED); // 情况2
x = parentOf(x); // 情况2
} else {
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK); // 情况3
setColor(sib, RED); // 情况3
rotateRight(sib); // 情况3
sib = rightOf(parentOf(x)); // 情况3
}
setColor(sib, colorOf(parentOf(x))); // 情况4
setColor(parentOf(x), BLACK); // 情况4
setColor(rightOf(sib), BLACK); // 情况4
rotateLeft(parentOf(x)); // 情况4
x = root; // 情况4
}
} else { // 跟前四种情况对称
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK); // 情况5
setColor(parentOf(x), RED); // 情况5
rotateRight(parentOf(x)); // 情况5
sib = leftOf(parentOf(x)); // 情况5
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED); // 情况6
x = parentOf(x); // 情况6
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK); // 情况7
setColor(sib, RED); // 情况7
rotateLeft(sib); // 情况7
sib = leftOf(parentOf(x)); // 情况7
}
setColor(sib, colorOf(parentOf(x))); // 情况8
setColor(parentOf(x), BLACK); // 情况8
setColor(leftOf(sib), BLACK); // 情况8
rotateRight(parentOf(x)); // 情况8
x = root; // 情况8
}
}
}
setColor(x, BLACK);
}
4 红黑树与AVL树的比较
先从复杂度分析说起,任意节点的黑深度(Black Depth)是指当前节点到NIL(树尾端)途径的黑色节点个数。根据约束条件的第(4)、(5)条,可以推出对于任意高度的节点,它的黑深度都满足:Black Depth ≥ height / 2。也就是说,对于任意包含n个节点的红黑树而言,它的根节点高度h ≤ 2log2(n+1)。常规BST操作比如查找、插入、删除等,时间复杂度为O(h),即取决于树的高度h。当树失衡时,时间复杂度将有可能恶化到O(n),即h = n。所以,当我们能保证树的高度始终保持在O(logn)时,便能保证所有操作的时间复杂度都能保持在O(logn)以内。
红黑树的平衡性并不如AVL树,它维持的只是一种大致上的平衡,并不严格保证左右子树的高度差不超过1。这导致在相同节点数的情况下,红黑树的高度可能更高,也就是说,平均查找次数会高于相同情况下的AVL树。在插入时,红黑树和AVL树都能在至多两次旋转内恢复平衡。在删除时,由于红黑树只追求大致上的平衡,因此红黑树能在至多三次旋转内恢复平衡,而追求绝对平衡的AVL树,则至多需要O(logn)次旋转。AVL树在插入与删除时,将向上回溯确定是否需要旋转,这个回溯的时间成本最差可能为O(logn),而红黑树每次向上回溯的步长为2,回溯成本降低。因此,面对频繁的插入和删除,红黑树更为合适;面对低频修改、大量查询时,AVL树将更为合适。
参考文献
- http://data.biancheng.net/view/192.html
- https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91
- https://tech.meituan.com/2016/12/02/redblack-tree.html
- https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/5-TreeSet%20and%20TreeMap.md
- https://www.zhihu.com/question/37381035
- 《码出高效:Java开发手册》