红黑树(Red Black Tree) 是一种自平衡二叉查找树,相对于普通的二叉树具备经过自旋和变色来保持树两端保持平衡的特色,从而得到较高的查找性能。 红黑树的最坏状况运行时间也是很是良好的,而且在实践中是高效的: 它能够在O(log n)时间内作查找,插入和删除。html
在正式介绍红黑树前,先简要介绍下二叉查找树(BST),二叉排序树或者是一棵空树,或者是具备下列性质的二叉树:node
以下图就是一个典型的二叉查找树 bash
基于上面提到BST存在的问题,一种新的树--平衡二叉树(Balanced BST)被提了出来。平衡二叉树在插入和删除的时候,会经过旋转操做将树的高度一直保持在logN。具备表明性的平衡二叉树有两种,分别为AVL树和红黑树,AVL树由于性能更差的缘故,在实际运用的状况下远不如红黑树。数据结构
红黑树(Red-Black Tree,如下简称RBTree)的实际应用很是普遍,常见的函数库,如C++ STL中,不少部分(包括set, multiset, map, multimap)应用了红黑树的变体,以及Java中的TreeMap,TreeSet, Java8中的HashMap的实现也将链表替换成了红黑树。函数
RBTree为了保持树严格的平衡性质,在原来BST的基础上添加了一下五点性质:性能
class TreeMapEntry{
K key;
V value;
TreeMapEntry<K,V> left;
TreeMapEntry<K,V> right;
TreeMapEntry<K,V> parent;
boolean color = BLACK;
}
RBTree节点的数据结构
复制代码
RBTree在本质上仍是一棵BST树,可是它在插入和删除数据的时候会经过变色和自旋来保持树的平衡,即保证树的高度在[logN,logN+1],将树的查找时间复杂度始终保持在logN,同时RBTree的插入和删除时间复杂度也都是logN,因此RBTree的查找接近于理想的BST。学习
RBTree的自旋主要是由于插入或删除后节点的颜色不符合上述的五条性质,致使树总体不平衡,须要经过自旋对树进行降层保持树的平衡网站
Java的RBTree就是一个典型的红黑树例子,下面也就TreeMap的源码来对解析RBTree的插入和删除操做ui
每一个新插入的节点都是红色的,若是插入的父节点是黑色的,那么操做结束。若是父节点是红色,那么则违反了规则3:每一个红色节点的两个子节点都是黑色
,则须要改变父类的颜色,若是父类颜色和祖父类冲突,那么就须要继续变色,甚至是自旋来使树节点颜色符合规则。spa
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
// 若是当前没有数据,就用此点当作根节点
if (t == null) {
compare(key, key);
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry<K,V> parent;
// 比较大小的方式,若是已经自定义过就用本身设定的,否则就用系统默认的比较方式
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//找到插入的父节点,生成当街节点,而后根据大小放在左节点仍是右节点
TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 数据插入完成后开始对树进行颜色平衡处理
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
复制代码
fixAfterInsertion
//这里对颜色的处理其实只看一半就好了,你会发现else后面的代码和上面是同样的,只不过
//左右作一下镜像处理
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
//新添加的节点设为红色
x.color = RED;
/** **/
由于规则3:每一个红色节点的两个子节点都是黑色。新添加的节点都为红色,
那么循环条件 要加上父类不为红色
当新加入的点为根节点时也不必循环了,直接最后面设置为黑色便可
**/
while (x != null && x != root && x.parent.color == RED) {
//若是当前父节点为祖父节点的左节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//这个y是祖父节点的另外一个节点,从关系上来讲,就是添加节点的叔叔节点
TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
/**
若是叔叔节点为红色,那么一样由于规则3可知祖父节点必定为黑色
此时把父节点设为黑色和叔叔节点 设为黑色,把祖父节点设为红色
这么作的主要目的是为了符合规则4:从任一节点到其每一个叶子的全部路径都包含相同数目的黑色节点
把祖父节点变成红色后,由于不知道祖父的父节点颜色,由于可能会违反规则3(祖父的父节点为红色),因此须要对祖父节点进行循环校验
**/
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
/*
* 若是叔叔节点为黑色,由于当前节点和父节点都为红色,祖父节点也必定为黑色
* 这种状况必定是通过上面那种状况变色后得出来的,由于叔叔和祖父是黑色,父亲节点是红色,
* 这种状况就违反了规则4:从任一节点到其每一个叶子的全部路径都包含相同数目的黑色节点
* 由于此时的叔叔节点比父节点多了一个黑色,这种状况只有多是由于原来父节点是黑色的,
* 因为添加了新节点后由于上面的变换setColor(parentOf(parentOf(x)), RED);致使的
*
* 这种状况下单纯变色已经无论用了,只能经过自旋来平衡
* 先把父节点变黑,祖父节点变红
* 这时候经过右旋把父节点的右孩子变成祖父节点的左孩子,这时候达到颜色平衡,详情后面看动图例子
*
*
* 上面的操做是正常状况下的操做,可是在这个操做前须要先作一个判断
* 若是当前节点是父类的右孩子,那么须要对父节点进行左旋转
* 由于若是不作这个操做的话,因为当前节点是红色的,上面操做【右旋把父节点的右孩子变成祖父节点的左孩子】
* 也就是把当前节点给了祖父的左孩子,可是由于祖父节点已经被设为了红色,这样两个红色节点就违反了规则3,
* 因此若是当前节点是父类的右孩子时须要对父节点进行左旋,
* 左旋后当前节点变成了父节点,父节点变成了左孩子,红色的节点也移到了左边,移到祖父节点的也是黑色的,不会起冲突
* 这样,最多经过两次自旋就能够解决冲突
*/
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//这里就不赘述了,原理如出一辙,只是方向相反罢了
TreeMapEntry<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;
}
复制代码
枯燥的讲解永远没有生动的例子有效,下面简单举几个例子
while (x != null && x != root && x.parent.color == RED) {
TreeMapEntry<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));
复制代码
经过结合代码能够看到添加新节点25后,由于父节点(50)是红色的,进入循环。又由于叔叔节点是红色的,因此将父节点和叔叔节点设为黑色,祖父节点设为红色,再将当前节点x设为祖父节点,由于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)));
}
复制代码
这里能够看到添加一个新数据5后,第一步由于父节点(10)和叔叔节点(26)为红色,因此作了一次变色,把父节点(10)和叔叔节点(26)变黑,祖父节点(25)变红,当前节点x变为祖父节点(25)
这里x(25)变红色后和父节点(50)颜色起了冲突。可是为何这里不能继续经过变色来平衡呢,由于这里若是将25或者50节点中一个变为黑色后,那么最左侧这一条路径的黑色节点数量就比其余路径黑色节点数量多,因此这时候就须要进行自旋
因此将父节点(50)设为黑色,将祖父节点(100)设为红色,而后右旋,。右旋完成后,由于父节点(50)变成了黑色,退出了循环,平衡完成
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)));
}
复制代码
通过变色后能够看到叔叔节点(150)为黑色,因此须要进行自旋。可是又由于x(75)是父节点(50)的右孩子,因此须要对父节点(50)进行左旋,同时将当前节点x设为50。
第一次左自旋后,当前节点x为50,而后将左旋后的父节点(75)设为黑色,祖父节点(100)设为红色,而后对祖父节点(100)进行右自旋,右自旋后当前节点的父节点为(75)为黑色,退出循环,平衡完成
学习红黑树的过程当中最困惑的不是自旋和变色,而是自旋和变色的时机,钻了很多死胡同。这里总结一下(仅当是左边树状况下,右边树状况只要将左右颠倒便可):
一、当叔叔节点为红色时,将新加节点和父节点变色便可
二、当叔叔节点为黑色时,若是当前节点位于左节点,那么将父节点变黑,祖父节点变红,而后右旋便可。
三、当叔叔节点为黑色时,若是当前节点位于右节点,那么须要先以父节点左旋,而后/2操做便可
删除操做相对于插入多了一层复杂度,但仍是有迹可循。
删除操做会先删除节点,若是是叶子结点就直接删除,若是非月子节点,会先遍历找到该点删除后的继承点。在删除节点后,须要作修复操做,使树从新达到颜色和高度平衡。
修复操做是只有在删除黑色节点时才有,由于删除黑色节点会违反规则4:从任一节点到其每一个叶子的全部路径都包含相同数目的黑色节点
。须要作的处理是从兄弟节点上借调黑色的节点过来,若是兄弟节点没有黑节点能够借调的话,就只能往上追溯,将每一级的黑节点数减去一个,使得整棵树符合红黑树的定义。
删除操做的整体思想是从兄弟节点借调黑色节点使树保持局部的平衡,若是局部的平衡达到了,就看总体的树是不是平衡的,若是不平衡就接着向上追溯调整。
private void deleteEntry(TreeMapEntry<K,V> p) {
modCount++;
size--;
/**
* 若是被删除的节点P非叶子节点,那么须要在删除他以前找到合适的点来继承这个节点位置
*/
if (p.left != null && p.right != null) {
//找到继承点后,将被删除点的值都赋给继承点,这里注意了,继承点s的关系赋值给了p
//变成了要删除的点
//这个节点是找位于p点右子树的最小数节点
TreeMapEntry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
/**
* 若是进入了上面的循环,最后的p是位于删除节点右子树的最左端点 ,并且有且最多只有一
* 个右子节点,这时候这个``replacement``就是就是p.right
*
* 若是没有通过上面的循环,那么被删除节点有且最多只有一个子节点
* 并且这个惟一的子节点必然是叶子节点(由于若是该节点还有子节点就违反规则4了)
* 这时候用这个叶子节点替换便可
*/
TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
p.left = p.right = p.parent = null;
/**
* 这里两种可能
* 1:若是该P有两孩子,那么他变成了找继承者出来的s,若是他是黑色节点,
* 那么等因而删除节点后面缺了一个黑色s(拿去顶删除节点位置了),因此须要调整树
*
* 2:若是该p没有两个孩子节点,那么他就是被删除的节点,删除了黑色节点就意味着有一边树少了一个黑色
* 比另外一边树总体路径就少了一个黑色节点,因此也须要调整树
*
* 若是两种状况都是红色的话,对总体没有影响,因此不须要变色
*
*/
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // return if we are the only node.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
/**
* 若是被删除节点p没有孩子节点,若是p点是黑色,由于删除了黑色节点违反了规则4
* 那么须要调整树,若是p点为红色,那么直接删除便可
*/
if (p.color == BLACK)
fixAfterDeletion(p);
//颜色调整完毕后将p和其余节点断开联系,而后删除
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
复制代码
找寻删除节点后合适的继承点
static <K,V> TreeMapEntry<K,V> successor(TreeMapEntry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
TreeMapEntry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
TreeMapEntry<K,V> p = t.parent;
TreeMapEntry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
复制代码
fixAfterDeletion
//这里和前面同样,看一半逻辑就好了,另外一半逻辑是镜像对称的
private void fixAfterDeletion(TreeMapEntry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
TreeMapEntry<K,V> sib = rightOf(parentOf(x));
/**
* 这里和插入操做不同的地方是着重判断兄弟节点的颜色,而不是叔叔的
* 若是兄弟节点是红色,那么由于本身这边删除了一个黑色,总体黑色就比兄弟那边少一个、
* 这时候须要将父节点变红,兄弟节点变黑,经过左旋从兄弟节点那边借一个黑色节点过来
*/
if (colorOf(sib) == RED) {
状况一
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
/**
* 这里注意,就算兄弟节点没有孩子节点,他的左右树链接的是null,null节点也是黑色的
* 这里的判断只是判断他是否有红色孩子节点
* 若是兄弟节点是红色的,通过上面步骤变化,这里兄弟节点变成了红色
*/
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
状况二
//在兄弟节点是黑色且两孩子都是黑色的状况下,须要将兄弟节点变红
//继续循环调整其父节点
setColor(sib, RED);
x = parentOf(x);
} else {
/**
* 进入这个右孩子是黑色的判断,那么左孩子必定是红色(由于null节点也是黑色)
* 将兄弟节点变红,左孩子(红色)变黑,右旋将红色节点移到右子树
*/
if (colorOf(rightOf(sib)) == BLACK) {
状况三
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
//通过右旋后 找出新的叔叔节点
sib = rightOf(parentOf(x));
}
状况四
/**
* 到了这一步,兄弟节点是黑色的,兄弟节点的左孩子是黑色的,右孩子是红色的
* 这时候删除节点后须要从兄弟节点那边先借节点过来
* 先把兄弟节点颜色赋值为父节点颜色,再把兄弟节点的右孩子和父节点变为黑色
* 由于左旋前父节点之前都是平衡的,兄弟节点左旋后替代父节点的位置就要和父节点颜色一致
* 而后兄弟节点的位置由兄弟节点的右孩子接替了,颜色要变成黑色,
* 左孩子,也就是左旋前的父节点也要变得和上面接替的右孩子同样变成黑色,这样就平衡了
* 最后设为root 退出循环,
*/
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
TreeMapEntry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
setColor(x, BLACK);
}
复制代码
在演示实例前,先总结下删除操做的思想:找到删除节点后的的继承者,将继承者的值赋给被删除节点,这时候把删除节点变为继承者,被删除节点没有子节点就直接删除,而后维持树的平衡。主要分为两个步骤
第一步:将红黑树看成一颗二叉查找树,将节点删除。
replacement
为他的孩子节点。successor
。找到继承点后用继承点S替换被删除点X,replacement
M则为继承点S的左或右孩子节,若S没有孩子后续操做和操做1同样,若是有则后续操做和操做2同样,这样就回到了最初的问题。第二步:经过自旋和从新填色等一系列操做来修正树,使之从新成为一棵红黑树。
上面动图演示的那个网站的红黑树源码是从左子树找最大点来当继承点,而TreeMap是从右子树找最小点来当继承点,因此动图就不适合用了,因此我根据TreeMap画了个自定义view,可是没有动态效果了,仅供展现。
if (p.left != null && p.right != null) {
TreeMapEntry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
复制代码
上图这种状况下,由于被删除点P(50)有两个孩子节点,因此就须要为他找继承successor
, 这个点就是P右子树的最小点也就是75。而后把被删除点的值变为继承点的值,而后被删除点P就变成了原successor
(75)
由于如今的P没有左右子树,因此replacement
也就为null,此时P点为黑色点,因此须要fixAfterDeletion
平衡树
P点在右子树,因此找到兄弟节点Sib(25),Sib节点为黑色且左右孩子(null)都为黑色,此时向上追溯,将P点的父节点也就是原P(红色的50,现红色的75)继续循环。但此时P点就变成了红色,退出循环,在最后setColor(x, BLACK);
将红色的75变为黑色,最后删除P(75)即完成了树的平衡。
这个例子我举完才发现和上面的步骤如出一辙,只不过找寻successor
的时候多向下找寻了一层而已,详细操做看上个例子解析
总结下:当被删除的节点为红色时,操做很简单,若是只有一个子节点,就将子节点替换上来。若是有两个子节点,就经过找寻successor
,将successor
的值赋给被删除点,而后将被删除点替换为successor
便可。
这种状况下比较简单,由于P点无孩子节点,也就不存在successor
和replacement
。由于P点这边树删除了一个黑色节点,就比兄弟树少了一个黑色节点,此时就须要从兄弟那边借一个黑色节点过来。
因此将sib(75)设为黑色,父节点(60)设为红色,而后左旋,这样左子树就从右子树借了个黑色节点(70)过来。而后此时的sib点从新赋值给了70,为了助于理解,这里看下图
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
复制代码
左旋后如图,P点是待删除点,70成了新的sib点。此时能够发现若是P点被删了那么最左侧这边树总体就少了一个黑色节点。此时sib的左节点P由于子节点都为黑色(null)因此将sib(70)设为黑色,将P点设为其父节点,由于父节点(60)为红色,因此退出循环,最后setColor(x, BLACK);
将60设为黑色,再将P点删除即完成平衡。
和上个例子同样,也不存在找继承点的状况,直接来平衡树。
由于sib(70)是黑色,sib()的左右节点都为黑色(null),因此直接sib(75)设为红色,而后追溯用father(60)带入继续循环。
和上一步原理同样,此时的sib(150)黑色,左右节点也为黑色,因而将sib(150)设为红色,将父节点(100)带入继续循环,但由于100已是root节点,因此退出循环,删除p点完成平衡
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
复制代码
这里看似树很简单,可是这个操做是最复杂的一个,这里要进行两步自旋,下面就主要解释下做用。
当sib节点有两个子节点,可是其颜色不一致时,此时左旋从sib借黑色节点时可能会把红色节点给过去,因此要若是红色节点在左边时先须要作个右旋将红色节点移到右边。
右旋完成后,如今要从sib这边借黑色节点,由于左旋后sib节点就成了原来的父节点,sib右节点就成了原来的sib节点,因此为了保持颜色平衡,让他们分别继承他们要接替位置的颜色,而原来的父节点就接替了被删除点P的位置,因此将其颜色设为黑色,至此,平衡完成。
当删除一个有子节点的黑色节点时,将这个操做分解后问题就变成了上面【删除没有孩子节点的黑色节点】和【删除红色节点】,而后继续上面的操做便可,这里给出了三个例子。
图一:删除点P(60):找到其继承点successor
(70),赋值变换后这个问题就变成了:删除一个黑色节点70,且他的兄弟节点为黑色
图二:删除点P(60):找到其继承点successor
(65),replacement
点为70,赋值变换后这个问题就变成了:删除一个红色节点70
图二:删除点根节点(100):找到其继承点successor
(125),赋值变换后这个问题就变成了:删除一个黑色节点125
总结:当删除的黑色节点无子节点时,根据兄弟节点的颜色来作具体操做,当删除有子节点的黑色节点时,能够经过找继承点将问题分别为删除一个无子节点的黑色或红色节点,因此问题就顺利简化了。
东西仍是本身再写一遍才能看出本身是否真正理解了,在看了几篇文章后我觉得我理解了红黑树,因此我尝试着按本身理解去写一篇笔记,结果写的过程当中发现看似理解,实则根本描述不出其缘由,于是从新又去梳理了一遍,有了不少新的感悟。写的比较凌乱,如若是以为文章有错误,欢迎指出并交流。
附上红黑树演示图网址