跟着TreeMap学红黑树
最近面试被问了两三次红黑树,感觉红黑树还是有必要好好学一下的,因为TreeMap是用红黑树实现的,所以打算跟着TreeMap的源码学习一下红黑树,基于jdk1.8源码。
红黑树的介绍
红黑树是一种类平衡的树,红黑树和平衡树的区别是:平衡树是严格平衡的,对于任意一个节点n,n的左子树和右子树的高度差不得超过1,红黑树是非严格平衡的,对于任意一个节点,从当前节点开始,向左的最长路径记为ll,向右的最长路径记为lr,假设ll<lr,那么lr-ll<=ll,反之同理。
红黑树有以下几条性质(规则)
1.任意节点要么为红色,要么为黑色
2.根节点颜色为黑色
3.对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点
4.红色节点不能连续,及红色节点的父节点和子节点不能为红色
5.叶子节点(称为null节点或nil节点)默认为黑色
TreeMap的源码
因为红黑树在进行插入或者删除时,可能会破坏上述的规则,所以需要调整,与调整有关的方法主要有4个,分别为rotateLeft(Entry<K,V> p),rotateRight(Entry<K,V> p),fixAfterInsertion(Entry<K,V> x),fixAfterDeletion(Entry<K,V> x)。所以这四个方法是看源码过程中最重要的四个方法,也比较有难度。
红黑树的节点
private static final boolean RED = false;
private static final boolean BLACK = true;
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//键值
V value;//value值
Entry<K,V> left;//指向左子节点的指针
Entry<K,V> right;//指向右子节点的指针
Entry<K,V> parent;//指向父节点的指针
boolean color = BLACK;//节点的颜色,因为有两种取值,所以用boolean表示
//BLACK为true,RED为false,默认为黑
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
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;
}
}
从上可以看出,因为map中存放的是键值对,所以Entry中有key和value,此外,由于要实现红黑树,所以需要用一个布尔值color来表示颜色,另外需要三个指针,分别指向当前节点的父节点,左子节点和右子节点。
左旋和右旋
因为在调整红黑树时需要左旋和右旋,所以先来讲解一下这两个操作
左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的指针。旋转之后,二叉查找树的属性仍然满足。
左旋过程:
左旋举例:
左旋代码:
左旋在TreeMap中是通过rotateLeft(Entry<K,V> p)函数来实现的,下面贴上代码:
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
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;
}
}
上述代码实现的功能是:
下面详细分析一下上述代码,代码可分为3个步骤来理解:
1)4-6行功能是处理r的左子节点,
2)7-13行的功能是处理r的父节点,
3)14-15行的功能是处理p和r的关系。
接着详细分析一下指针的改变过程,这里画出所有的指针,以及指针改变的过程,为了解释方便,画出p的父节点pp,并假设p为pp的左子节点,因为左旋过程只是改变指针,所以这里没有考虑颜色
初始状态:
步骤1结束之后的状态(去掉了两条指针,新增了两条指针):
步骤2结束之后的状态(也是去掉了两条指针,新增了两条指针):
现在有必要整理一下上图,调整某些节点的位置,已经去掉的指针不再显示:
可以看到,只有两条指针的指向不正确,而这正是步骤3所做的工作。
步骤3结束之后的状态,即为最终结果:
右旋与左旋类似,如果左旋明白,右旋代码应该可以写出来,这里不再详细介绍。
右旋过程与举例:
右旋代码:
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;
}
}
插入操作
代码:
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 {
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);
}
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;
}
插入的步骤就是找到适当的位置,然后插入这个节点,如果小,往左走,如果大,往右走,如果相等,覆盖旧的value并返回旧的value值。
最重要的是在插入之后要执行fixAfterInsertion(e);这行代码,它的作用就是如果需要的话,调整红黑树,使其满足所有的规则。下面看代码。
代码:
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;//将当前节点涂红(初始值为黑),为了满足规则3
while (x != null && x != root && x.parent.color == RED) {//如果x和其父节点都为红色,进入while循环
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {//如果x的父节点为x的祖父节点的左子节点,分三种情况讨论
Entry<K,V> y = rightOf(parentOf(parentOf(x)));//取x的叔叔节点,即x的祖父节点的右子节点,记为y
if (colorOf(y) == RED) {//情况1:y为红色
setColor(parentOf(x), BLACK);//将x的父节点涂黑
setColor(y, BLACK);//将y涂黑
setColor(parentOf(parentOf(x)), RED);//将x的祖父节点涂红
x = parentOf(parentOf(x));//将x指向x的祖父节点,开始下一轮while循环
} else {
if (x == rightOf(parentOf(x))) {//情况2:y为黑色,且x为其父节点的右子节点
x = parentOf(x);//将x指向x的父节点
rotateLeft(x);//对x进行左旋
}
//情况3:y为黑色,且x为其父节点的左子节点,可以看出,执行情况2之后就会接着执行情况3
setColor(parentOf(x), BLACK);//将x的父节点涂黑
setColor(parentOf(parentOf(x)), RED);//将x的祖父节点涂红
rotateRight(parentOf(parentOf(x)));//对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;//最后,确保根节点为黑色(规则2)
}
最后,举几个例子,走一下上面的代码
假设初始状态为下图,满足情况1(x和其父节点都为红色,x的叔叔节点为红色):
那么,执行情况1的几个步骤,即将5涂黑,8涂黑,7涂红,x指向7,结果如下:
现在,发现当前状态并不满足跳出循环的条件(x和其父节点都为红),进入循环,发现满足情况2(x和其父节点都为红色,x的叔叔节点为黑色,x为其父节点 的右子节点),执行情况2的几个步骤,即将x指向x的父节点(即x指向2),然后对2进行左旋,结果如下:
发现现在的状态满足情况3(x和其父节点都为红色,x的叔叔节点为黑色,x为其父节点 的左子节点),而代码的逻辑也是紧接着就执行情况3,完美,那么现在执行情况3的几个步骤,即将7涂黑,11涂红,对11进行右旋,结果如下:
现在发现,当前状态不满足循环条件,退出循环,将根节点涂黑(确保根节点为黑色,这个例子中本就是黑色,但有可能出现退出循环之后,根节点为红色的情况),调整结束。也可以检查一下当前状态,发现红黑树的规则都是满足的。
至此,插入过程的调整已经讲完,总结一下可能的调整路径
满足情况1时,调整路径可能为1->2->3(及刚才的例子),1->3,1这三种
满足情况2时,调整路径只能为2->3
满足情况3时,调整路径只能为3
下面,对1->3这条路径再找一个例子(其他路径的例子都很好找,不再举例):
最后,再总结一下,三种情况及对应的执行步骤:
情况1:x和父节点都为红色,叔叔节点为红色
执行步骤:将父节点和叔叔节点涂黑,祖父节点涂红,将x指向祖父节点
情况2:x和父节点都为红色,叔叔节点为黑色,x为其父节点的右子节点
执行步骤:将x指向父节点,对x进行左旋
情况3:x和父节点都为红色,叔叔节点为黑色,x为其父节点的左子节点
执行步骤:将父节点涂黑,祖父节点涂红,对祖父节点进行右旋
注意:以上总结都是针对x的父节点为祖父节点的左子节点而言的,对另一种对称的情况,即x的父节点为祖父节点的右子节点,是一样的道理,本文没有讨论。
至此,红黑树的插入及插入之后的调整已经介绍完毕,删除及删除之后的调整过几天再说吧,今天不想看了,哈哈。