前面一章我们聊到了平衡二叉树,它是一种搜索效率极高的树, 但是它有一个缺点就是建树成本、 插入节点以及删除节点都需要进行 左平衡和右平衡旋转,需要消耗大量的计算资源。所以引进了红黑树,它相对平衡二叉树,牺牲了一小部分的搜索效率,但是换来了在建树、插入节点和删除节点的极大的方便。 这就是红黑树的由来。
红黑树的定义
它是以棵空树,或者是一个具有以下性质的树:
- 节点非红即黑
- 根节点是黑色
- 所有的叶子节点都是NULL节点,且节点颜色是黑色
- 所有的红节点的子节点必须是黑色节点
- 从任意节点到其叶子节点的所有路径上都包含有相同数目的黑节点
满足上面的定义的它就是一棵红黑树。
红黑树相对另外一个平衡二叉树,还有另外一个特点:
相对于平衡二叉树的改进,任意一个节点,他的左右子树的层次最多不超过一倍。
如上图:根节点 13,左边是4层, 那么右边最多是7层
如17号节点,左边是两层,那么右边就最多3层。
红黑树的建树规则
按照二叉排序树的方式插入节点,初始颜色是红色。
一、插入的是根节点 直接涂黑
二、如果插入的节点的父节点是黑色,不做任何处理
如上图, 1 和 2 号位置标记的,选择为 1 号节点插入一个 -1节点, 不做任何处理,也满足上面的五条红黑树的规则
同样的 15号节点插入一个左子节点14,也满足上面的五条红黑树的规则。
三、插入节点的父节点是红色
如果满足这种情况,我们需要根据父亲节点时祖父节点的左孩子还是右孩子以及祖父节点的另一个子节点(叔叔节点)是红色还是黑色分情况判断。
-
父亲节点时祖父节点的左孩子
-
祖父节点的另一个子节点(叔叔节点)是红色
对策: 将当前节点的父节点和叔叔节点涂黑,祖父节点涂红,把当前节点指向祖父节点,从新的当前节点重新开始判断( 三这个步骤) -
叔叔节是黑色
-
如果当前节点是其父节点的右孩子
对策:当前节点的父节点做为新的当前节点,以新当前节点为支点左旋 在执行情况二 -
如果当前节点是其父节点的左孩子
对策:父节点变为黑色,祖父节点变红色,再祖父节点为支点进行右旋
-
-
-
父亲节点时祖父节点的右孩子
- 祖父节点的另一个子节点(叔叔节点)是红色
对策: 将当前节点的父节点和叔叔节点涂黑,祖父节点涂红,把当前节点指向祖父节点,从新的当前节点重新开始判断( 三这个步骤)
如上图完成变换以后,发现当前节点时红色且为根节点,没有父节点,最后把 当前节点涂黑,即完成红黑树的变换
- 叔叔节是黑色
- 情况一、当前节点是其父节点的左孩子
对策:当前节点的父节点做为新的当前节点,以新当前节点为支点右旋。然后在执行情况二
- 情况二、当前节点是其父节点的右孩子
对策:父节点变为黑色,祖父节点变红色,再祖父节点为支点进行左旋
- 情况一、当前节点是其父节点的左孩子
- 祖父节点的另一个子节点(叔叔节点)是红色
通过TreeMap查看红黑树的建树规则
因为TreeMap的底层就是用的红黑树数据结构,使我们非常好的学习例子
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
// 1、如果根 等于 null,就直接设置根节点
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//如果有设置比较器,if 和else 代码是相同的 所以
if (cpr != null) {
// 注释1
do { // while通过比较器从根节点位置做比较,如果比t小,t就往左边找,反之就往右边找, 用parent 记录位置
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);
//跳出wihle循环以后,插入节点,且节点颜色是红色
if (cmp < 0)
parent.left = e;
else
parent.right = e;
//核心方法,因为插入的节点就会不满足,红黑树的规则,这个时候就需要对红黑树进行调整
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
从上面的注释可以看出这就是一个 排序二叉树的插入规则,从根节点开始遍历,小就往左边找,大往右边找,然后把t 指向 t 的做节点或者右节点。直到t等于null,跳出循环,说明已经找到了位置。
/** From CLR */
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
//注释1 新插入的节点 首先给一个红色,然后后面对这节点修改颜色,同时 x 的指向也会改变
x.color = RED;
//注释2
//如果父节点是黑色不做操作,把插入节点改成黑色,即可
//如果父节点是红色,进入while
while (x != null && x != root && x.parent.color == RED) {
// 注释2.1 父节点是祖父节点的左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
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));
} else {
//不是红色 ,就会执行下面这两步
//变换1
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
//变换2
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else { // 注释2.2 父节点是祖父节点的左孩子
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 {
//叔叔节点时黑色
//变换1
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
//变换2
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
其实通过上面的代码 更加清晰明白红黑树的建树规则。