TreeMap
version:1.8
TreeMap基于红黑树实现对存储的键值对的快速查询,其在多线程下使用不安全。
红黑树是一种平衡二叉搜索树。
二叉搜索树是一种特殊的二叉树,其每个节点的值大于其左子节点的值,小于其右子节点的值(当然也可能反过来),从而可以类似二分查找快速查找到想要的值。
若二叉搜索树节点的左子树与右子树的节点数目相差过大,会影响查找效率,极端情况下每个节点只有右子节点,这个时候二叉树已经退化为链表。
平衡二叉搜索树通过设置一些限制尽量保证每个节点的左子树与右子树的节点数目相差尽量小,从而保证搜索效率。
红黑树为每个节点设置颜色(红 / 黑),添加如下条件保证平衡:
- 每个节点不是黑色就是红色
- 根节点的颜色为黑色
- 红色节点的直接子节点必为黑色(红-黑),反之不要求
- 每条从根节点到叶子节点的路径经过的黑色节点的数量相同
此处需注意:若某个子节点有左子节点或右子节点,但没有另一边的节点,那么另一边的空节点也认为是叶子节点。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
…………
}
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
…………
}
get
首先看下最简单的操作:查询
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
就是根据值的大小不断缩小查找范围(时间复杂度:O(log n)):
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;
}
使用Comparator比较:
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
K k = (K) key;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
Entry<K,V> p = root;
while (p != null) {
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}
put
put的难度要远大于get:
整体可分为四部分:
- 红黑树为空时
- 寻找插入的位置
- 插入
- 调整
第一步很简单,创建一个节点即可。
第二步与get类似,需注意若存在相同的key,替换对应的value。
第三步与第一步类似。
最关键的是第四步,调整使得树的结构仍然满足红黑树的四个特性。
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); //存在相同的key,替换掉
} 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;
}
插入调整
接下来详细看下插入调整的源码,即方法:fixAfterInsertion
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
首先设置插入节点的颜色为红色,如此可能造成红-红冲突,但此时所有路径黑色节点数量不变。
因此需要解决红-红冲突。
接下来我们看到有个大循环:
循环条件 x.parent.color == RED 代表存在红-红冲突。
while (x != null && x != root && x.parent.color == RED)
循环内部首先分为两种情况:
1. x的父节点是x的祖父节点的左子节点
内部又分为两种情况:
1.1 x的祖父节点的右子节点颜色为红色
如上图所示:
此时由于红-黑规则,祖父节点必然为黑色节点。
if (colorOf(y) == RED) { //1.1
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
}
接着看源码:进行了颜色的调整,p与y设为黑色,g设为红色。
如此避免了p与x可能的红-红冲突,但此时祖父节点(红色)与其父节点可能会冲突,故向上回溯,继续调整。
1.2 x的祖父节点的右子节点为空 或 颜色为黑
直接插入节点y节点颜色不可能为黑色:
若其为黑色,x1与x2两个位置原来有个位置为空(否则无法插入),若两个都为空,到p与到y两条路径的黑色节点数量不同,若一个不为空,其必为黑色,但到另一个空叶子节点的路径与到y两条路径的黑色节点数量不同。即必然不满足要求,故此时y节点必为空节点。
且此时p节点原来必没有子节点,否则到其与到y节点路径的黑色节点数量不同,冲突。
但在1.1向上回溯时可能造成y节点颜色为黑。
else {
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
接着看源码:
若x在x2位置上(右子节点),首先进行左旋。
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;
}
}
程序中的r为图中的x节点:
即变成上图所示样子。(注意该函数没有要求p是g的左子节点还是右子节点)
注意在向上回溯的过程中x节点可能会有自己的子节点,需要保证旋转后树仍然满足二叉搜索树。
经过左旋,此时的结构本质与x为父节点的左子节点一样。
在这里或许会有个疑问,上图中x节点与下图中x节点位置有差异,接下里程序运行是否会产生问题:再看下代码,左旋前,引用x已指向原节点的父节点,左旋后到了下图中x节点的位置。
此时仍然存在红-红冲突。
接下来首先将父节点与祖父节点的颜色翻转,然后进行右旋。
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;
}
}
如下图所示:不再存在红-红冲突,经过各条路径的黑色节点数量不变,此时调整完毕,无需向上回溯。
2. 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)));
}
}
最后设置根节点为黑色(在回溯过程中可能导致其变为红色)。
完整源码如下:
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)))) { //情况1
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) { //1.1
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else { //1.2
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
}
else { //情况2,与1对称
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;
}
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 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 = 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
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;
// Null out links so they are OK to use by fixAfterDeletion.
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.
root = null;
} else { // No children. Use self as phantom replacement and unlink.
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;
}
}
}
if (p.left != null && p.right != null) {
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}
一步步看,首先若该节点有两个子节点,调用successor方法:
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
该方法并不复杂,找该节点的中序后继节点(即排序后下一个值):其右子节点的最左子节点(由于调用方法的前提是节点具有两个子节点,在这里即返回) 或其向上最左父节点的父节点。
将找到节点的值赋给当前节点的位置,不改变二叉搜索树的性质,然后删除掉找到的节点即可。
这一步找到的节点由于为其右子节点的最左子节点,即该节点最多有一个右子节点,如此,将要删除的节点由有两个子节点转换为最多有一个子节点,从而减小了删除的难度。
接下来的删除操作又分为了三种情况 :
最简单的就是第二种,该节点没有子节点且为根节点,将根节点赋值为null即可。
else if (p.parent == null) { // return if we are the only node.
root = null;
}
然后就是第三种,该节点没有子节点但其有父节点,删除该节点不会导致红-红冲突,但由于删除的节点颜色可能为黑色,删除掉可能会造成经过每条路径的黑色节点数量不同,需要调整。
else
{ // No children. Use self as phantom replacement and unlink.
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;
}
}
最后看一看第一种情况,即该节点存在一个子节点,主要就是调整下各个节点间的连接。
同样若删除的节点颜色为黑色,需要进行调整(此时不同于第二种情况,可能产生红-红冲突)(若其为红色,其父节点与子节点不会为红色,删除节点不会导致红-红冲突)。
需注意传入调整方法的节点与第三种情况不同,且第二种情况先调整再移除,这种情况先移除再调整。
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
if (replacement != null) {
// Link replacement to parent
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;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
}
删除调整
类似于插入,删除真正复杂的部分在删除后的树结构的调整,即方法fixAfterDeletion:
由前文我们知道
- 删除的节点没有子节点,此时传入的就是要被删除的子节点(尚未被删除)
- 删除的节点有一个子节点,此时传入的就是其子节点(要删除的节点已被删除)
先考虑一种最简单的情况,删除的节点有一个子节点且其为红色(在这种情况也是唯一一种可能在删除后产生红-红冲突),那么设置该子节点为黑色即可,即解决了其分支对应的路径黑色节点数量少了一个的问题,也避免了产生红-红冲突。
看如下源码,循环未执行,设置x(replaceme)黑色,即为此种情况。
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 = rightOf(parentOf(x));
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
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;
}
}
}
setColor(x, BLACK);
}
但实际情况有很多种 ,代码很长,但与插入调整类似,分成了两大类情况,且由代码可以看出是对称的。
故看一半就可以,此时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 = rightOf(parentOf(x));
// }
if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// if (colorOf(rightOf(sib)) == BLACK) {
// setColor(leftOf(sib), BLACK);
// setColor(sib, RED);
// rotateRight(sib);
// sib = rightOf(parentOf(x));
// }
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
}
代码中注释掉了两段,因为按插入调整代码的推测,这两段代码会将树的结构调整为if对应的另一种情况(更简单),故先不管它们,看下简单的情况,然后再看这些代码如何调整树的结构。
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
// !(colorOf(sib) == RED)
if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// !(colorOf(rightOf(sib)) == BLACK)
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
}
现在代码已经很短了。
先看第一个if:
此时设置sib(sister/brother?)为红色,那么p节点下两条分支黑色节点数量就一致了,接下来就需要继续向上回溯:举个例子,若此时p为红色,将其调整为黑色,那么调整就完毕了。
接下来接着看else:
图中p可能为g的右子节点,但不产生影响。
经过此番调整,g节点的此分支经过x节点的路径的黑色节点数目加1,其它的不变,调整完毕,x赋值root,结束循环。
看到这,不得不说,想出这种方法的人真是强。
接下来再看看被忽略掉的两个if:
第一个:
调整sib颜色为黑色
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
由于sib为红色,p与sl必然为黑色,经过调整,sl 作为新的sib,颜色变为黑色,经过各条路径的黑色节点数量不变。
第二个:
调整sib的右子节点为红色
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
此时sl为红色(否则满足 (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) )
s与slr,sll必为黑色,如下图所示,经过调整后新的sib为sl,其右子节点变为了红色(其左节点即sll为黑色),各条路径经过黑色节点数量不变。
如此,这一半代码就读完了,另一半对称的,就不再详细介绍了。