前言
上一章的HashMap并没有提到红黑树,就是因为本章的TreeMap就是一棵红黑树。TreeMap是存储键值对(key-value结构)的自平衡二叉树,又称红黑树。TreeMap的key是有序且不可为空的,但是value是可以为空的。TreeMap的类图结构如下
TreeMap类上的注释有两个地方需要注意:
1.TreeMap是一个基于NavigableMap实现的红黑树,TreeMap的排序方式有两种:(1)基于key的自然排序(2)在创建TreeMap的时候提供一个比较器,使用哪种排序取决于你使用的哪种构造方法,也就是默认无参构造方法基于自然排序,如果key是自定义对象,要么你的自定义对象是已经实现了Comparable接口或在创建TreeMap时提供一个Comparator的实现
/**A Red-Black tree based {@link NavigableMap} implementation.
* The map is sorted according to the {@linkplain Comparable natural
* ordering} of its keys, or by a {@link Comparator} provided at map
* creation time, depending on which constructor is used.
*/
2.TreeMap的实现不是同步的,其实也就是多线程不安全的。如果多线程同时访问一个映射,并且进行了结构性的修改,那么它必须时同步的(结构性修改是指添加或者删除一个或多个映射,修改已经存在的key的值不算是结构性修改)。并且注释给出的多线程安全使用TreeMap的方式是SortedMap m = Collections.synchronizedSortedMap(new TreeMap(…));
/**<p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a map concurrently, and at least one of the
* threads modifies the map structurally, it <em>must</em> be synchronized
* externally. (A structural modification is any operation that adds or
* deletes one or more mappings; merely changing the value associated
* with an existing key is not a structural modification.) This is
* typically accomplished by synchronizing on some object that naturally
* encapsulates the map.
*/
红黑树TreeMap
红黑树的定义
首先红黑树是一棵特殊的二叉查找树,二叉查找树的每个节点键值大于左孩子的键值,小于或等于右孩子的键值;二叉查找树在特殊情况下所有子节点都在左边或者右边,这被称为二叉查找树的左倾或右倾,极端情况下,二叉查找树就变成了一个有序的链表了,而红黑树呢通过对二叉查找树的着色,将二叉查找树变成了一棵相对平衡的树,不会出现左倾或右倾的情况:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 [注意:这里叶子节点,是指为空的叶子节点!]
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
TreeMap中红黑树的节点元素包含自身的键值对,左右孩子节点,父节点,节点默认黑色满足条件3
TreeMap如何添加元素
TreeMap的put方法
public V put(K key, V value) {
// 取类对象实例的根节点
Entry<K,V> t = root;
// 若根节点为空
if (t == null) {
// 调用比较方法,看看当前key值是否可比较,不可比较抛出ClassCastException,key为空抛出NullPointerException异常
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;
// 必须有比较器,这里这个key的非空和比较校验其实可以提前,毕竟下面寻找节点位置设置值的代码重复了,没必要
if (cpr != null) {
/**
* 从根节点循环遍历比较key的值
* 1.若插入的key的值比当前节点的key值小,取当前节点的左子节点
* 2.若插入的key的值比当前节点的key值大,取当前节点的右子节点
* 3.若插入的key的值等于当前节点的key的值,则设置值覆盖之前的值
* 4.若遍历的节点为空,则结束遍历
*/
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);
// 如果key小于遍历节点的key值
if (cmp < 0)
// 将该节点的左子节点设置为新建节点
parent.left = e;
// 如果key大于遍历节点的key值
else
// 将该节点的右子节点设置为新建节点
parent.right = e;
// 插入节点完成之后进行着色
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
从以上代码来看,TreeMap的put方法的前一部分跟二叉查找树的新增节点没有区别,先按照节点的key的值的顺序,寻找节点的位置,然后创建节点,改变父节点的孩子节点的引用,然后完成这个动作之后,再给节点着色
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
x = parentOf(x);
// 当前节点的父节点右旋,新增节点取带了当前父节点的位置,原父节点变为了新增节点的右子节点
rotateRight(x);
}
// 设置父节点的颜色为黑色(其实是新增节点)
setColor(parentOf(x), BLACK);
// 设置爷爷节点的颜色为红色
setColor(parentOf(parentOf(x)), RED);
// 爷爷节点左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 着色有可能会改变根节点和根节点的颜色,将根节点设置为黑色
root.color = BLACK;
}
默认新增节点都是红色的,为了满足定义4,所以父节点为红色的情况下,必须要进行上层节点的着色修改了,上层节点着色就出现了几种情况了:
1.正常来说红黑树的同级节点的颜色应该是一致的,所以如果大伯节点和叔叔节点都是红色的情况,只需要重新着色就好
2.如果父节点是红色,而叔叔节点或者大伯节点的颜色为黑色,说明大伯节点或者叔叔节点有子节点,这时候证明新增节点之后这棵树长歪了必须掰正,这个就是红黑树的自平衡了,而这种情况下又分了两种情况:
1.新增节点的父节点为爷爷节点的左子节点,如果新增节点为父节点的右子节点,父节点为红色,按照红黑树的定义默认新增节点为红色,则很容易推出当前节点的同级左子节点为空节点,这时候树长歪了,所以当前节点需要左旋,同时爷爷节点也长歪了,需要右旋
2.新增节点的父节点为爷爷节点的右子节点,如果新增节点为父节点的左子节点,父亲也为红色,同理也很容易推断,当前同级节点的右子节点也为空节点,所以当前节点右旋,爷爷节点左旋
接下来我们看看左旋和右旋的方法:
/**
* 左旋
*/
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;
}
}
/**
* 右旋
*/
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;
}
}
总结一下左旋和右旋:
左旋:左旋即将旋转节点的右子节点取代自己的位置,同时将该节点的左子节点过继给旋转节点的右子节点,并且将旋转节点设置为该节点的左子节点
右旋:右旋即将旋转节点的左子节点取代自己的位置,同时将该节点的右子节点过继给旋转节点的左子节点,并且将旋转节点设置为该节点的右子节点
TreeMap如何删除元素
TreeMap的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;
}
首先遍历红黑树,拿到当前需要删除的节点,如果节点不存在,直接返回null;
private void deleteEntry(Entry<K,V> p) {
// 操作版本+1
modCount++;
// 元素个数减1
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
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
// 将取代的节点连接到p节点的父节点
replacement.parent = p.parent;
// 如果p节点为空
if (p.parent == null)
// 取代的节点就是根节点
root = replacement;
else if (p == p.parent.left)
// 如果p节点为左节点,那么取代节点就是其父节点的左节点
p.parent.left = replacement;
else
// 如果p节点为右节点,那么取代节点就是其父节点的右节点
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
// 置空p节点的所有关联关系和数据
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.
// 如果p节点是唯一节点,则置空当前TreeMap的根节点
root = null;
// p节点为红色节点,即p无子节点,因为默认添加的子节点为红色,按照红黑树的定义红色节点是不可能有红色子节点的,所以红色节点没有子节点
} else { // No children. Use self as phantom replacement and unlink.
if (p.color == BLACK)
// 删除之后的重新着色
fixAfterDeletion(p);
// 清空父节点与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;
}
}
}
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
// 当前节点为空,直接返回
return null;
// 如果当前节点的右子节点不为空
else if (t.right != null) {
// 将当前节点的右子节点赋值给P
Entry<K,V> p = t.right;
// 循环遍历p节点的左子树,并取左子节点作为P的值
while (p.left != null)
p = p.left;
// 返回P,即遍历之后的最左的不为空的左子节点
return p;
} else {// 当前节点的右子节点为空的情况
// 将当前节点的父节点赋值给p
Entry<K,V> p = t.parent;
// 将当前节点的值赋值给ch
Entry<K,V> ch = t;
// 如果父节点不为空,并且当前节点是父节点的右节点
while (p != null && ch == p.right) {
// 将父节点的值赋值给ch
ch = p;
// 取父节点的父节点赋值给p
p = p.parent;
}
// 直到p为空,或者ch不是p的右子节点,返回P
return p;
}
}
/**
* 删除节点之后的重新着色
*/
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) {
// 设置兄弟节点为黑色
setColor(sib, BLACK);
// 父亲节点为红色
setColor(parentOf(x), RED);
// 左旋父亲节点
rotateLeft(parentOf(x));
// 将父亲节点赋值给sib
sib = rightOf(parentOf(x));
}
// 如果sib的左右孩子节点为黑色
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
// 设置sib节点为红色
setColor(sib, RED);
// 将其父节点赋值给x
x = parentOf(x);
// sib的子节点的颜色不都是黑色
} else {
// 右子节点为黑色
if (colorOf(rightOf(sib)) == BLACK) {
// 将左子节点也改为黑色
setColor(leftOf(sib), BLACK);
// sib节点修改为红色
setColor(sib, RED);
// 右旋sib节点
rotateRight(sib);
// sib重新赋值
sib = rightOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
// 父节点设置为黑色
setColor(parentOf(x), BLACK);
// sib右子节点也设置为黑色
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
sib = leftOf(parentOf(x));
}
// 如果左右子节点为黑色
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
// 设置sib为红色
setColor(sib, RED);
// 将其父节点设置为x
x = parentOf(x);
// 如果左右子节点不都为黑色
} else {
// 左子节点为黑色
if (colorOf(leftOf(sib)) == BLACK) {
// 将右子节点设置为黑色
setColor(rightOf(sib), BLACK);
// 将sib节点设置为黑色
setColor(sib, RED);
// 左旋sib
rotateLeft(sib);
// 将其父节点的左子节点设置给sib
sib = leftOf(parentOf(x));
}
// 将其父节点的颜色设置给sib
setColor(sib, colorOf(parentOf(x)));
// 父节点颜色设置为黑色
setColor(parentOf(x), BLACK);
// sib左子节点设置为黑色
setColor(leftOf(sib), BLACK);
// 右旋父节点
rotateRight(parentOf(x));
// 将root赋值给x
x = root;
}
}
}
// 保证旋转过后根节点的颜色一定还是黑色
setColor(x, BLACK);
}
TreeMap的删除的过程要比新增的过程复杂很多,删除需要讨论的情况比较多,不过不管红黑树怎么改变,使用的手段都是左旋和右旋,以及重新着色,并且这个过程一定要满足红黑树的定义
总结
红黑树是一棵有序的二叉查找树,红黑树是线程不安全的。在源码阅读debug调试的过程中,新增的情况下,我一直没有找到出发左旋和右旋的案例,另外红黑树适合需要进行元素的查找和统计的方案下存储数据,一般需要有序的key-value情况可以使用LinkedHashMap.