前言
红黑树(Red Black Tree
) 是一种自平衡二叉查找树。JDK1.8中,当HashMap的链表达到一定长度后,会将链表转化为红黑树。同时,TreeMap中数据的存储结构就是红黑树。
红黑树
红黑树定义
红黑树是一个平衡的二叉树,但不是一个完美的平衡二叉树。红黑树是在普通二叉树上,对每个节点添加一个颜色属性形成的,同时整个红黑二叉树需要同时满足一下五条性质 :
- 节点是红色或者是黑色 在树里面的节点不是红色的就是黑色的,没有其他颜色;
- 根节点是黑色 根节点总是黑色的;
- 每个叶节点(NIL或空节点)是黑色;
- 每个红色节点的两个子节点都是黑色的 连续的两个节点不能是连续的红色;
- 从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点
红黑树平衡性修正
红黑树主要通过三种方式对平衡进行修正,改变节点颜色、左旋和右旋。
变色
改变节点颜色,这是为满足上述第四个条件而设定的修正操作。事实上,在红-黑树中插入的节点都是红色的,这不是偶然的,因为插入一个红色节点比插入一个黑色节点违背红-黑规则的可能性更小。
左旋
左旋的过程是将父节点的右子树绕父节点逆时针旋转,使得父节点的右子树成为父节点的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
右旋
右旋的过程是将父节点的左子树绕父节点顺时针旋转,使得父节点的左子树成为父节点的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
红黑树的优势
红黑树的查询性能略微逊色于AVL树,因为它比AVL树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上完爆AVL树,AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多。
TreeMap中红黑树应用
TreeMap
TreeMap的实现是红黑树算法的实现。
定义
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
重要属性
//比较器,因为TreeMap是有序的,
//通过comparator接口我们可以对TreeMap的内部排序进行精密的控制
private final Comparator<? super K> comparator;
//TreeMap红-黑节点,为TreeMap的内部类
private transient Entry<K,V> root;
//容器大小
private transient int size = 0;
//TreeMap修改次数
private transient int modCount = 0;
//红黑树的节点颜色--红色
private static final boolean RED = false;
//红黑树的节点颜色--黑色
private static final boolean BLACK = true;
节点结构
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
TreeMap的关键方法
get方法
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
TreeMap的get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.value。因此getEntry()是算法的核心。算法思想是根据key的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0的entry。
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 {
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;
}
上述代码中,首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要:1.改变某些节点的颜色;2.对某些节点进行旋转。
private void fixAfterInsertion(Entry<K,V> x) {
//新节点的颜色是红色
x.color = RED;
//当x不为根节点,并且x的父节点为红色时需要调整
while (x != null && x != root && x.parent.color == RED) {
//判断x的父节点是否为x祖父节点的左子节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取x的叔父节点,即x父节点父节点的右子节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//如果x的叔父节点为红色,
//调整x的父节点和叔父节点为黑色,
//调整x的祖父节点为红色,
//调整结束后,x指向祖父节点。
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) {
//如果x是其父节点的右子节点,
//x指向其父节点,
//以x为中心进行左旋操作
x = parentOf(x);
rotateLeft(x);
}
//调整x父节点颜色为黑色,
//调整x祖父节点颜色为红色,
//以x的祖父节点为中心进行右旋操作
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//x的父节点是x祖父节点的右子节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔父节点为红色,
//处理逻辑跟上述x的父节点是x祖父节点的左子节点情形一样
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
//如果x是其父节点的左子节点,
//x指向其父节点,
//以x为中心进行右旋操作
x = parentOf(x);
rotateRight(x);
}
//调整x父节点颜色为黑色,
//调整x祖父节点颜色为红色,
//以x的祖父节点为中心进行左旋操作
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//根节点颜色为黑色
root.color = BLACK;
}
总的来说,当前新增节点不为空,且不为根节点,并且父节点颜色为红色时需要循环调整红黑树结构
。
调整时,新增节点所处的位置特征有三种:
- 插入节点的父节点和其叔父节点(祖父节点的另一个子节点)均为红色的;
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的右子节点;
- 插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。
第一种情况,修改父节点和叔父节点颜色为黑色,祖父节点颜色为红色,置祖父节点为新插入节点,以待后续处理;
第二种情况,置插入节点的父节点为先插入节点,并以此为中心进行左旋操作;
第三种情况,置祖父节点为红色,置父节点为黑色,并以祖父节点为中心进行右旋操作。
remove方法
红黑树的删除节点处理逻辑可以分为两步:以二叉查找树的逻辑删除节点;红黑树性质的修复。
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
if (p.left != null && p.right != null) {
//查找节点p的“后继”节点,其条件如下:
//p的右子树不空,则t的后继是其右子树中最小的那个元素。
//p的右孩子为空,则t的后继是其第一个向左走的祖先。
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
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;
p.left = p.right = p.parent = null;
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
root = null;
} else {
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;
}
}
}
删除过程可以简单分为两种情况:
- 删除点p的左右子树都为空,或者只有一棵子树非空。
- 删除点p的左右子树都非空。
对于第一种情况,直接将p删除(左右子树都为空时),或者用非空子树替代p(只有一棵子树非空时);对于第二种情况,可以用p的后继s(树中大于x的最小的那个元素
)代替p,然后使用第一种情况的处理办法删除s(此时s一定满足情况1,可以画画看)。
当删除节点为黑色的时候,才会触发调整函数fixAfterDeletion,通常有如下四种情况:
- 当前节点是黑色的,且兄弟节点是红色的(那么父节点和兄弟节点的子节点肯定是黑色的);
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的;
- 当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色。
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
//x为左子节点,并且兄弟节点为红色,
//调整兄弟节点为黑色,父节点为红色,
//以父节点为中心左旋操作,sib指向左旋操作后x的兄弟节点
//此时节点x和其兄弟节点为黑色,父节点为红色
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
//如果兄弟节点的两个子节点都为黑色
//调整兄弟节点为红色
//x指向x自身的父节点
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
//如果兄弟节点的右子节点为黑色
//调整兄弟节点的左子节点为黑色
//调整兄弟节点颜色为红色
//以兄弟节点为中心进行右旋操作
//sib指向节点x的兄弟节点
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
//调整sib的颜色为x节点父节点颜色
//调整x节点父节点颜色为黑色
//调整sib右子节点颜色为黑色
//以x的父节点为中心左旋操作,并将x指向root
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
Entry<K,V> 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;
}
}
}
//调整节点x颜色为黑色
setColor(x, BLACK);
}
x节点为右子节点的情况可以对称处理,不再赘述。
总结
本文阐述了红黑树的定义、性质以及红黑树的典型实现——TreeMap,详细描述了TreeMap在发生结构变化时的调整逻辑,希望读者能有所启发。