本篇红黑树文章,近120多幅图构成,让你了解红黑树不再难。
发表是最好的记忆 --候捷
目录:
红黑树介绍
旋转分析
插入分析
删除分析
完整实例宏微观图解过程
5.1 插入宏微观图解过程
5.2 删除宏微观图解过程
代码实现分析
分析过程附件
先看下《算法导论》对RBTree的介绍:
红黑树,一种二叉查找树,但在每个节点上增加一个存储位表示节点的颜色,可以是Red或Black。
通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有1条路径会比其它路径长出2倍,因而是接近平衡的。
下面,在具体介绍红黑树之前,我们先来简单了解下,二叉查找树的一般性质:
(1)在一棵二叉查找树上,执行查找、插入、删除等操作的时间复杂度为O(logN); 因为,一棵由n个节点,随机构造的二叉查找树的高度为logN, 所以顺理成章,一般操作的执行时间O(logN);
至于n个节点的二叉树高度为logN的证明,可参考《算法导论》
(2)但若是一棵具有n个节点的线性链,则这些操作最坏情况运行时间为O(n);
而红黑树,能保证在最坏情况下,基本的动态操作的时间均O(logN);
我们知道,红黑树上每个节点内含有5个域,color, key, left, right, p。 如果相应的指针域没有,则设为NIL。
一般的,红黑树,满足以下性质,即只有满足以下全部性质的树,我们才称之为红黑树:
每个节点要么是红,要么是黑色;
根节点是黑色;
每个叶节点,即空节点(NIL)是黑色;
如果一个节点是红色,那么它的2个孩子节点必为黑色;
对于每个节点,从该节点到其子孙的叶子节点的路径上包含相同数目的黑色节点;
下图所示,即是一颗红黑树:
左旋与右旋
为什么要左旋右旋?
因为红黑树插入或删除节点后,树的结构发生了 变化,从而可能破坏红黑树的性质。为了维持插入或删除节点后的树,仍然是一棵红黑树,所以有必要对树的结构做部分调整,从而恢复红黑树的原本性质。而为了恢复红黑树性质而作的动作包括节点颜色的改变(重新着色),和节点的调整。
这部分节点调整工作,改变指针结构,即是通过左旋或右旋而达到目的。
从而使插入或删除节点的树重新成为一棵新的红黑树。
请看下图:
左旋代码实现,分三步:
右旋代码类似,可自行对比类比分析。
接下来,我们来了解,红黑树的【删除】操作。
《算法导论》一书,给的算法实现:
红黑树删除的几种情况:
以下所有的操作,是针对红黑树已经删除节点之后,为了恢复和保持红黑树原有的5点性质,所做的恢复工作。
前面,我已经说了,因为插入,或删除节点后,可能会违背,或破坏红黑树的原有的性质,
所以为了使插入,或删除节点后的树依然维持为一棵性的红黑树,那就要做2方面的工作:
(1)部分节点颜色,重新着色;
(2)调整部分指针的指向,即左旋,右旋;
而下面所有的文字,则是针对红黑树删除节点后,所做的修复红黑树性质的工作:
情况1:当前节点是红色。
对策:直接把当前节点染成黑色,结束。
此时红黑树性质全部恢复。
情况2:当前节点是黑色且是根节点
对策:什么都不做,结束;
情况3:当前节点是黑色,且兄弟节点为红色(此时父节点和兄弟节点的子节点分别为黑)。
对策:把父节点染成红色,把兄弟节点染成黑色,之后重新进入算法(我们只讨论当前节点是其父节点左子的情况)
然后,针对父节点做一次左旋。此变换后原红黑树性质5不变,而把问题转化成兄弟节点为黑的情况。
变化前【删除2节点】:
变化后:
情况4:当前节点是黑色,且兄弟节点是黑色,且兄弟节点的两个子节点全为黑色。
对策:把当前节点和兄弟节点中抽取一重黑色追加到父节点上,把父节点当成新的当前节点,重新进入算法
变化前【删除2节点】:
变化后:
情况5:当前节点颜色是黑色,兄弟节点是黑色,兄弟的左子是红色,右子是黑色。
对策:把兄弟节点节点涂红,兄弟左子涂黑,之后再在兄弟节点为支点右旋,之后重新进入算法。此时把当前的情况转化为情况6,而性质5得以保持:
变化前【删除2节点】:
变化后:
情况6:当前节点颜色是黑色,它的兄弟节点的右子是红色,兄弟节点左子的颜色任意。
对策:把兄弟节点涂成当前节点父节点的颜色,把当前父节点涂成黑色,兄弟节点右子涂成黑色,之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确。
变化前【删除2节点】:
变化后:
首先,各个结点插入与以上的各种插入情况,一一对应起来,如图:
以下的20个插入元素过程的图例,是依次插入这些结点:12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17的全程演示图,已经把所有的5种插入情况,都全部涉及到了:
1.插12
(1) 情形1:
(1) 情形2:
3. 插9
(1)情形4(Case2):
(2)情形5(Case3):
4.插2
(1)情况3(Case1):
5.插0
(1)情形2:
6.插入11
(1) 情形2:
7.插入7
(1)情形3(Case1):
8.插入19
(1)情形2:
9.插入4
(1)情形4(Case2):
(2) 情形5 (Case3):
10.插入15
(1)情形3(Case1):
红黑树的一一插入各结点:12 1 9 2 0 11 7 19 4 15 18 5 14 13 10 16 6 3 8 17的全程演示图完。
-----插入完毕-----
package zhuguozhu.algods;
import java.util.*;
/**
* 红黑树
*
* @Author guozhu_zhu
* @Date 2020/3/30 14:47
*/
public class TreeMap001, V> {
private transient Entry root;
private transient int size = 0;
public TreeMap001() {
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean containsKey(K key) {
return getEntry(key) != null;
}
public boolean containsValue(Object value) {
for (Entry e = getFirstEntry(); e != null; e = successor(e)) {
if (valEquals(value, e.value)) {
return true;
}
}
return false;
}
public V get(K key) {
Entry p = getEntry(key);
return (p == null ? null : p.value);
}
public K firstKey() {
return key(getFirstEntry());
}
public K lastKey() {
return key(getLastEntry());
}
final Entry getEntry(K key) {
if (key == null) {
throw new NullPointerException();
}
Entry p = root;
while (p != null) {
int cmp = key.compareTo(p.key);
if (cmp > 0) {
p = p.right;
} else if (cmp < 0) {
p = p.left;
} else {
return p;
}
}
return null;
}
public V put(K key, V value) {
Entry t = root;
if (t == null) {
root = new Entry<>(key, value, null);
size++;
return null;
}
int cmp;
Entry parent;
if (key == null) {
throw new NullPointerException();
}
do {
parent = t;
cmp = key.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 e = new Entry(key, value, parent);
if (cmp < 0) {
parent.left = e;
} else {
parent.right = e;
}
fixAfterInsertion(e);
size++;
return null;
}
public V remove(K key) {
Entry p = getEntry(key);
if (p == null) {
return null;
}
V oldValue = p.value;
delteEntry(p);
return oldValue;
}
public V replace(K key, V value) {
Entry p = getEntry(key);
if (p != null) {
V oldValue = p.value;
p.value = value;
return oldValue;
}
return null;
}
static final boolean valEquals(Object o1, Object o2) {
return (o1 == null ? o2 == null : o1.equals(o2));
}
static K keyOrNull(Entry e) {
return (e == null) ? null : e.key;
}
static K key(Entry e) {
if (e == null) {
throw new NoSuchElementException();
}
return e.key;
}
// RB-TREE
private static final boolean RED = false;
private static final boolean BLACK = true;
static final class Entry {
K key;
V value;
Entry left;
Entry right;
Entry parent;
boolean color = BLACK;
Entry(K key, V value, Entry 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;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry, ?> e = (Entry, ?>)o;
return valEquals(key, e.getKey()) && valEquals(value, e.getValue());
}
@Override
public int hashCode() {
int keyHash = (key == null) ? 0 : key.hashCode();
int valueHash = (value == null)? 0 : value.hashCode();
return keyHash ^ valueHash;
}
@Override
public String toString() {
return key + "=" + value;
}
}
final Entry getFirstEntry() {
Entry p = root;
if (p != null) {
while (p.left != null) {
p = p.left;
}
}
return p;
}
final Entry getLastEntry() {
Entry p = root;
if (p != null) {
while (p.right != null) {
p = p.right;
}
}
return p;
}
// 返回被删除节点继承者节点
static Entry successor(Entry t) {
if (t == null) {
// 如果被删除节点为空,则直接返回null
return null;
} else if (t.right != null) {
// 如果被删除节点的右子节点不为空
// 将被删除节点的右子节点记录下来
// 从该节点开始循环向下查找最左子节点,直到查找到叶子节点后返回叶子节点
Entry p = t.right;
while (p.left != null) {
p = p.left;
}
return p;
} else {
// 如果被删除节点的右子节点为空
// 向上回溯搜索
// 将被删除节点用p变量记录
Entry p = t.parent;
Entry ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
static Entry predecessor(Entry t) {
if (t == null) {
return null;
} else if (t.left != null) {
Entry p = t.left;
while (p.right != null) {
p = p.right;
}
return p;
} else {
Entry p = t.parent;
Entry ch = t;
while (p != null && ch == p.left) {
ch = p;
p = p.parent;
}
return p;
}
}
private static boolean colorOf(Entry p) {
return p == null ? BLACK : p.color;
}
private static Entry parentOf(Entry p) {
return p == null ? null : p.parent;
}
private static Entry leftOf(Entry p) {
return p == null ? null : p.left;
}
private static Entry rightOf(Entry p) {
return p == null ? null : p.right;
}
private static void setColor(Entry p, boolean c) {
if (p != null) {
p.color = c;
}
}
// 左旋
private void rotateLeft(Entry p) {
if (p != null) {
Entry 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 if (p.parent.right == p) {
p.parent.right = r;
}
r.left = p;
p.parent = r;
}
}
// 右旋
private void rotateRight(Entry p) {
if (p != null) {
Entry l = p.left;
p.left = l.right;
if (l.right != null) {
l.right.parent = p;
}
l.parent = p.parent;
if (p.parent == null) {
p = l;
} else if (p.parent.left == p) {
p.parent.left = l;
} else if (p.parent.right == p) {
p.parent.right = l;
}
l.right = p;
p.parent = l;
}
}
// 树插入一个新节点后,将其根据红黑树的规则进行修正
private void fixAfterInsertion(Entry x) {
// 默认将当前插入树的节点颜色设置为红色, 为什么???
// 因为红黑树有一个特点:"从根节点到所有叶子节点上的黑色节点个数是相同的
// 如果当前插入的节点是黑色,那么必然会违反这个特性,所以必须将插入节点的颜色先设置为红色
x.color = RED;
// 第一次变量时,X变量保存的是当前新插入的节点
// 为什么要用while循环
// 因为在旋转过程中有可能还会出现父子节点均为红色的情况,所以要不断上变量直到整个树都符合红黑树的规则
while (x != null && x != root && x.parent.color == RED) {
// 如果当前节点不为空切不是根节点,并且当前节点的父节点颜色为红色
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 如果当前节点的父节点等于当前节点父节点的父节点的左子节点(即当前节点为左子树插入)
Entry y = rightOf(parentOf(parentOf(x)));
// 获取当前节点的叔父节点
if (colorOf(y) == RED) {
// 如果叔父节点的颜色为红色
// 以4步用来保证不会连续出现两个红色节点
// 将当前节点的父节点设置为黑色
setColor(parentOf(x), BLACK);
// 将当前节点的叔父节点设置为黑色
setColor(y, BLACK);
// 将当前节点的祖父节点设置为红色
setColor(parentOf(parentOf(x)), RED);
// 当前遍历节点变更为当前节点的祖父节点
x = parentOf(parentOf(x));
} else {
// 如果叔父节点的颜色为黑色,或没有叔父节点
// 如果当前节点为左子树内侧插入
if (x == rightOf(parentOf(x))) {
// 将x变更为当前节点的父节点
x = parentOf(x);
// 对当前节点的父节点进行一次左旋操作(旋转完毕后x对应的就是最左边的叶子节点)
rotateLeft(x);
}
// 如果当前节点为左子树外侧插入
// 将当前节点的父节点设置为黑色
setColor(parentOf(x), BLACK);
// 将当前节点的祖父节点设置为红色
setColor(parentOf(parentOf(x)), RED);
// 对当前节点的祖父节点进行一次右旋
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry 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;
}
private void delteEntry(Entry p) {
// 节点总数-1
size--;
// 1. 这里的情况是两个节点不为空,转化为存在1个子节点为空的情况,便于处理
if (p.left != null && p.right != null) {
// 当前要删除节点的左右节点都不为空,找后继节点
// 采用前驱替代也是可以,predecessor(p);
// https://www.cs.usfca.edu/~galles/visualization/RedBlack.html
// 这个网站的删除操作就是通过前驱替代的,而TreeMap中采用的是后继替代
Entry s = successor(p);
// 找到一个待删除节点的继承者节点s
p.key = s.key;
p.value = s.value;
p = s;
}
// 2. 替代节点选择为当前删除节点的左子节点或右子节点
Entry replacement = (p.left != null ? p.left : p.right);
// 2.1 替代节点(被删除节点的子节点)不为空
if (replacement != null) {
replacement.parent = p.parent;
// 如果被删除节点的父节点为null
if (p.parent == null) {
// 将根节点设置为替换节点
root = replacement;
} else if (p == p.parent.left) {
// 若原先被删除节点是父的左子
p.parent.left = replacement;
} else {
p.parent.right = replacement;
}
// 将被删除节点的左子节点、右子节点,父节点引用都设置为null
p.left = p.right = p.parent = null;
// 删除后要执行后续的保证红黑树规则的平衡调整
// 如果被删除节点是黑色
if (p.color == BLACK) {
// 调用删除后修正红黑树规则的方法
fixAfterDeletion(replacement);
}
} else if (p.parent == null) {
// 2.2 被删除节点就是树根节点,直接删除即可
root = null;
} else {
// 2.3 被删除节点没有节点可替代的情况(即被删除节点是叶子节点)
if (p.color == BLACK) {
fixAfterDeletion(p);
}
// 如果被删除节点的父节点不为null
if (p.parent != null) {
// 如果原先被删除节点是左子树插入
if (p == p.parent.left) {
// 删除节点后将删除节点父节点的左子节点设置为null;
p.parent.left = null;
} else if (p == p.parent.right) {
p.parent.right = null;
}
// 将被删除节点的父节点引用设置为null;
p.parent = null;
}
}
}
private void fixAfterDeletion(Entry x) {
// 循环遍历, X刚开始是为被删除节点
while (x != root && colorOf(x) == BLACK) {
// 如果当前遍历到的节点不是根节点且为黑色
// 如果当前遍历的节点是父节点的左子节点
if (x == leftOf(parentOf(x))) {
// 将当前遍历到的节点的父节点的右节点用sib(兄弟节点)保存
Entry sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
// 如果sib引用的节点是红色
// 将sib引用的节点设置为黑色
setColor(sib, BLACK);
// 将当前遍历到的节点的父节点设置为红色
setColor(parentOf(x), RED);
// 对当前遍历到的父节点进行一次左旋
rotateLeft(parentOf(x));
// sib节点变更为旋转后节点的父节点的右子节点
sib = rightOf(parentOf(x));
}
if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
// 如果sib引用的左右节点都是黑色
// 将sib引用的节点设置为红色
setColor(sib, RED);
// 下次遍历的节点变更为当前遍历到节点的父节点
x = parentOf(x);
} else {
// 如果sib引用节点的左右节点不全是黑色
if (colorOf(rightOf(sib)) == BLACK) {
// 如果sib引用节点的右节点为黑色
// 将sib引用的左子节点设置为黑色
setColor(leftOf(sib), BLACK);
// sib引用节点设置为红色
setColor(sib, RED);
// 对sib节点进行一次右旋操作
rotateRight(sib);
// sib引用的节点变更为当前遍历到的节点的父节点的右子节点
sib = rightOf(parentOf(x));
}
// 将sib引用节点的颜色设置为当前遍历到节点父节点的颜色
setColor(sib, colorOf(parentOf(x)));
// 将当前遍历到节点的父节点设置为黑色
setColor(parentOf(x), BLACK);
// 将sib引用节点的右子节点设置为黑色
setColor(rightOf(sib), BLACK);
// 对当前遍历到的节点的父节点进行一次左旋
rotateLeft(parentOf(x));
// 下一次遍历的节点变更为根节点
x = root;
}
} else {
// 当前遍历到的节点是其父节点的右子节点
// 与上述代码类似,可对比分析
Entry 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);
}
// 层次遍历
public ArrayList> levelOrder() {
ArrayList> resList = new ArrayList>();
ArrayList list = new ArrayList();
Queue queue = new LinkedList();
queue.offer(root);
int cur = 1;
int next = 0;
while (!queue.isEmpty()) {
Entry curNode = queue.poll();
list.add(curNode.value);
cur--;
if (curNode.left != null) {
queue.offer(curNode.left);
next++;
}
if (curNode.right != null) {
queue.offer(curNode.right);
next++;
}
if (cur == 0) {
cur = next;
next = 0;
resList.add(list);
list = new ArrayList();
}
}
return resList;
}
public static void main(String[] args) {
TreeMap001 tree = new TreeMap001();
for (int i = 0; i < 100; i++) {
tree.put(i, i);
}
tree.remove(16);
System.out.println(tree.containsValue(15));
System.out.println(tree.containsValue(16));
ArrayList> resList = tree.levelOrder();
for (ArrayList i : resList) {
System.out.println(i);
}
}
}
1.插入修正:
2.删除修正:
我们只要牢牢抓住红黑树的5个性质不放,而不论是树的左旋还是右旋,不论是红黑树的插入,还是删除,都只为了保持和修复红黑树的5个性质而已。
参考文献
[1] 算法导论(原书第3版)/(美)科尔曼(Cormen, T.H.)等著;殷建平等译.北京:机械工业出版社,2013.1.