红黑树是自己看源代码时候最先遇到的比较高级的数据结构,早就想写一篇关于红黑树的文章了,但是因为时间的关系,一直没有弄。。。
首先来说在什么地方用到了红黑树:
(1)nginx的定时事件用到了红黑树,用它来定位当前超时时间最近的节点(libevent采用的是小根堆)
(2)STL中的map底层就是采用红黑树实现的
(3)linux调度中对进程的管理有用到
(4)java的TreeMap采用红黑树实现(HashMap采用散列的方式实现)
上面就是现在我知道的红黑树用到的地方,那么接下来我们来说说红黑树的定义吧:
(1)红黑树是树形结构
(2)它是一颗二叉搜索树(自平衡,但是与AVL树相比,平衡要求没有那么高,但是它的统计查找复杂度与AVL差不多,但是插入删除就简单了一些)
《STL源码剖析》中的定义:
(1)每个几点不是红色就是黑色
(2)根节点是和黑色
(3)如果节点为红,那么它的子节点就必须为黑色
(4)任意节点到树的末尾的任何路径,经过的黑色节点的数目必须相同
着这篇文章中我们以java的TreeMap为例子来说明红黑树的使用
首先来看一下红黑树节点的定义:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left = null; //左子节点
Entry<K,V> right = null; //又子节点
Entry<K,V> parent; //父节点
boolean color = BLACK; //颜色
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
/**
* Returns the key.
*
* @return the key
*/
public K getKey() {
return key;
}
/**
* Returns the value associated with the key.
*
* @return the value associated with the key
*/
public V getValue() {
return value;
}
/**
* Replaces the value currently associated with the key with the given
* value.
*
* @return the value associated with the key before this method was
* called
*/
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
具体key和value的作用就很明显了吧,另外还有三个引用,分别指向两个子节点和一个父节点,那么画一个图的话,几点就大概长成这个样子:
我们首先从节点的插入说起吧,也就是TreeMap的put方法:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) { //如果根节点为空,那么插进来的这个节点直接就是根节点就是了
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
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 {
//一般情况下key是需要实现ccomparable接口的
if (key == null)
throw new NullPointerException();
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);
}
Entry<K,V> e = new Entry<>(key, value, parent); //构造新的节点
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e); //由于插入了节点,那么有可能导致树不满足红黑树的要求了,所以需要进行一些调整
size++;
modCount++;
return null;
}
其实就是很普通的二叉搜索树的插入过程,而且从函数的实现可以看出,key类型需要实现comparable接口,或者需要传入comparator才行,不然的话怎么插入啊。。。
另外在前面说到了红黑树的定义,有许多的性质需要满足,因而插入新的节点可能就会导致原来的树不再满足红黑树的性质了,因此需要做一些调整,也就是fixAfterInsertion函数要做的事情。。
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;
}
上述函数就是使得刚刚插入新节点的红黑树通过一些列的旋转操作以及染色操作,使得新的树可以满足红黑树的要求,同时还可以使得树形结构保持相对的平衡,优化查找的时间复杂度。。。另外这里左旋转以及又旋转的动作,这里就以一张图来简单说明一下右旋转吧:
上图就是对节点(X1)进行了又旋转的效果。。。
好了,搞懂节点的插入,那么来看看节点的删除操作吧:
public V remove(Object key) {
//首先找到当前key的节点,其实也就是标准的二叉搜索树的查找过程
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p); //删除这个节点
return oldValue;
}
这个方法的实现还是非常简单的吧,也就是先通过查找找到这个key所对应的节点,查找过程也就是我们以前学数据结构的时候标准的二叉搜索树的查找过程,查找到之后,再调用deleteEntry方法,将这个红黑树节点删去。。。
这里在删除的时候需要注意一个概念,就是在树形结构中删除一个节点时候,总不可能让这里成为一个洞吧,那么就需要在当前的树中找到一个节点来作为替换节点,寻找当前删除节点的替换节点的方法是:
(1)如果当前删除节点有右子结点,也就是说存在又子树,那么又子树的最小的节点就可以成为替换节点
(2)当前删除节点没有又子节点,那么向上寻找,直到找到一个节点是其父亲节点的左儿子为止,或者到了根节点。。。
好了,我们接下来来看看deleteEntry方法定义吧:
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p); //获取当前节点的继承节点,并用继承节点来替换当前的节点
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right); //寻找一个替换节点,用于填充刚刚找到的继承节点
if (replacement != null) { //相当于是把刚刚的继承节点的子节点向上提,用于替换刚刚的继承节点
// Link replacement to parent
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;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null; //这里就相当于将继承节点删除了
// Fix replacement
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.
if (p.color == BLACK)
fixAfterDeletion(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;
}
}
}
首先找到要删除节点的继承节点,然后用继承的节点的key和value来替换当前删除的节点,然后再将继承节点删除就好了。。最后就还有fixAfterDeletion方法,用于修正删除了节点之后的红黑树的形状和颜色。。
好了,通过对java的TreeMap的一些分析,也算是对红黑树的东西有了一定的了解。。。下一篇看看小根堆吧。。