前言
之前写了有关于Java集合的总结,在Java集合中HashMap、TreeMap的底层实现都涉及到了红黑树。之前也听说是师兄在面试的时候也被问到有关于红黑树的部分,所以还是简单学习一下。
先介绍一个很不错的网站,可以模拟红黑树的插入和删除:
红黑树网站
红黑树的性质
红黑树的本质就是一颗不太严格的二叉查找平衡树。
也就是说它具有查找树的性质——小的数在左子树上,大的数在右子树上,以及平衡树的特性——高度差不超过1(在红黑树中是任意到叶节点的路径上的黑节点个数相同)。
那么它相比一般的平衡二叉查找树有什么优势呢?(感谢杰哥指出)
总的来说就是,红黑树在插入的时候并不像AVL树那样需要频繁的旋转,这样插入效率较得到了不少提升。
总的来说红黑树有如下特点:
- 根节点为黑色。
- 叶子节点是为黑色的空节点(也就是说在原来的叶子节点下面都加上了两个黑色的空节点)。
- 两个红色节点不能相邻(两个黑色节点可以)。
- 任意一条到叶子节点的路径上,黑节点个数相同。
红黑树的插入
红黑树由于插入之后需要维护其自身的特性,所以有如下操作:
- 变色:将一个节点的由红色变为黑色,或者由黑色变为红色。
- 旋转:当插入一个节点导致二叉树不能维护自己的平衡的时候需要进行旋转,分为左旋转和右旋转。
关于插入操作,我们结合Java中的TreeMap源码来看一下(其中的rotateLeft
和rotateRight
为左旋转函数和右旋转函数):
/** From CLR */
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;
}
第一步 判断父节点颜色
首先我们看到当进入这个函数的时候,把这个节点的颜色变为红色,这也符合我们之前说的当插入节点的时候把插入节点设置为红色。
我们看到源码中,最外面的while
循环里面的条件是x != null && x != root && x.parent.color == RED
,也就说当父亲节点是黑色以及插入节点是根节点的时候是不会进入这个循环的。
由此我们可以清楚的知道当插入节点的父亲节点为黑色或者插入节点是根节点的时候,可以不需要进行变色和旋转操作就可以维护红黑树的平衡。
那我们直接跳出循环外侧,可以看到最后一步是把根节点变为黑色,这是因为如果插入节点为根节点的话会使得根节点为红色,这里需要把它变成黑色(当然如果进入循环的话还有其他情况,这里说的是不进入循环的情况)。
结合这两张图来看一下,假设有如下红黑树,然后插入一个节点。
可以看到插入之后其实是不会影响红黑树的特性的。
第二步 父节点的位置和叔节点的颜色
在进入循环之后(说明此时父节点的红色节点),我们看到首先上来的一个条件判断就是if(parentOf(x) == leftOf(parentOf(parentOf(x)))
也就是说下一步的操作是判断插入节点的其祖父节点的左子节点还是其祖父节点的右子节点(此时父亲节点不可能是根节点,因为父亲节点是红色的)。我们可以看到无论是左子节点还是右子节点,代码都是相似的,区别就在于进行旋转的时候是左旋转还是右旋转。
这里我们假设父亲节点是祖父节点的左子节点,进入if判断。
进入if之后,下一行代码是rightOf(parentOf(parentOf(x)))
也就是获得插入节点的叔节点的颜色(当叔节点是叶子节点的时候也为黑色,所以不会出现叔节点不存在的情况)。
接下来的分类依据是叔节点的颜色,我们先来看比较短的情况,也就是叔节点是红色的情况:
//变色操作
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
显然部分的代码就是我们上面提到的变色操作,由此我们可以得出结论当叔节点为红色的时候(因为我们看到无论父节点是左子节点还是右子节点,这几行代码都是一致的),我们把插入节点的父节点、叔节点变为黑色,祖父节点变为红色,然后把祖父节点当做插入节点再进行一次平衡。
第三步 黑色叔节点
那如果是黑色叔节点呢?
我们选取父节点是右子节点的情况看一下代码:
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
这边可以看到,如果黑色叔节点,那么则需要进行旋转,进行旋转的时候又有讲究。为了好理解,我们先从简单的情况开始,所以我们直接跳过if
,看后面三行代码。
首先代码中进行了变色,把父节点变为黑色,祖父节点变为红色,然后对祖父节点进行一次左旋转(在GIF中是先旋转再变色)。
我们来模拟一下,在如下图红黑树中插入66节点。
注意此时x依旧是插入的节点(66节点),然后继续while
循环,发现此时父节点已经是黑色的了,所以直接退出循环。
第四步 黑色叔节点和左节点
那么最后来看最复杂的情况,插入节点是左节点并且叔节点是黑色的(父节点是右子节点)。
相比前面的情况多出了一段代码:
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
在这种情况下,首先先对插入节点和其父节点做一次右旋转,然后这个时候的x已经变成了其父亲节点(在GIF中插入节点是65,其父亲节点是66),这就又把问题转化为了第三步中的问题,也就是说接下来需要对新的插入节点(此时x是66节点)的父节点和祖父节点进行变色,然后进行一次左旋转。
插入总结
通过以上分析我们可以明白红黑树的插入过程。
- 首先判断插入节点是否是根节点、其父亲节点是否是黑色节点,如果是,则直接退出循环,把根节点变为黑色。
- 进入循环后,判断其叔节点是否是红色节点,如果是则进行变色,父节点变为黑色,祖父节点变为红色,然后把祖父节点当做插入节点再次循环。
- 如果叔节点是红色节点,判断插入节点的父节点是左节点还有右节点,然后如果在右节点中插入右节点和在左节点中插入左节点是一类,这种情况下找到插入节点的父节点和祖父节点进行变色,父节点变为黑色,祖父节点变为红色,然后对祖父节点进行一次旋转(右右节点左旋,左左节点右旋),然后退出循环(此时插入节点的父节点已经是黑色了)。
- 如果是左右节点或者右左节点,那么先对其父节点进行一次左(右)旋转,把x设置为其父节点,然后再执行3中的操作。
删除明天再写。