概述
TreeMap是红黑树的java实现,红黑树能保证增、删、查等基本操作的时间复杂度为O(lgN)。
首先我们来看一张TreeMap的继承体系图:
image
还是比较直观的,这里来简单说一下继承体系中不常见的接口NavigableMap和SortedMap,这两个接口见名知意。先说NavigableMap接口,NavigableMap接口声明了一些列具有导航功能的方法,比如:
Map.Entry firstEntry();
K lowerKey(K key);
K higherKey(K key);
通过这些导航方法,我们可以快速定位到目标的 key 或 Entry。至于 SortedMap 接口,这个接口提供了一些基于有序键的操作,比如:
SortedMap headMap(K toKey);();
SortedMap subMap(K fromKey, K toKey);
以上就是两个接口的介绍,很简单。关于TreeMap的继承体系就这里就说到这,接下来我们深入源码进行分析。
源码分析
添加
红黑树最复杂的无非就是增删了,这边我们先介绍增加一个元素,了解红黑树的都知道,当往 TreeMap 中放入新的键值对后,可能会破坏红黑树的性质。首先我们先巩固一下红黑树的特性。
接下来我们看看添加到底做了什么处理:
public V put(K key, V value) {
TreeMapEntry t = root;
if (t == null) {
if (comparator != null) {
if (key == null) {
comparator.compare(key, key);
}
} else {
if (key == null) {
throw new NullPointerException("key == null");
} else if (!(key instanceof Comparable)) {
throw new ClassCastException(
"Cannot cast" + key.getClass().getName() + " to Comparable.");
}
}
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry parent;
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);
}
TreeMapEntry e = new TreeMapEntry<>(key, value, parent);
if (cmp 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
这边会先把根节点暂存依赖,如果根节点为null,则讲新增的这个节点设为根节点。 如果初始化的时候指定了comparator比较器,则讲其插入到指定位置,否则使用key进行比较并且插入。不断的进行比较,找到没有子节点的节点,将其插入到相应节点。(注:如果查找出有相同的值只会更新当前值,cmp小于0是没有左节点,反之没有右节点。)
新插入的树破环的红黑树规则,我们会通过fixAfterInsertion去进行相应的调整,这也是TreeMap插入实现的重点,具体我们看看他是怎么通过java实现的。
private void fixAfterInsertion(TreeMapEntry x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
TreeMapEntry 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 {
TreeMapEntry 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;
}
首先将新插入的节点设置为红色,这边做了一个判断,新节点不为null,新节点不为根节点并且新节点的父节点为红色。才会进入内部的判断,因为其本身就是一个红黑树。如果新节点的父节点为黑色,则他依旧满足红黑树的特性。所以当其父节点为红色进入内部的判断。
如果新节点是其祖父节点的左子孙,则拿到其祖父节点的右儿子,也就是新节点的叔叔。如果叔叔节点是红色。则将其父节点设为黑色,讲叔父节点设为黑色,然后讲新节点直接其祖父节点。
否则如果新节点是其父节点的右节点,以其父节点进行左转,将父节点设为黑色,祖父节点设为红色,在通过祖父节点进行右转。
else内容和上述基本一致。自己分析~~
最后我们还需要将跟节点设为黑色。
我们稍微看一下,他是怎么进行左转和右转的。
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
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
private void rotateRight(Entry p) {
}
下面我们通过图解来看看如何插入一颗红黑树。
现有数组int[] a = {1, 10, 9, 2, 3, 8, 7, 4, 5, 6};我们要将其变为一棵红黑树。
首先插入1,此时树是空的,1就是根结点,根结点是黑色的:
然后插入元素10,此时依然符合规则,结果如下:
当插入元素9时,这时是需要调整的第一种情况,结果如下:
红黑树规则4中强调不能有两个相邻的红色结点,所以此时我们需要对其进行调整。调整的原则有多个相关因素,这里的情况是,父结点10是其祖父结点1(父结点的父结点)的右孩子,当前结点9是其父结点10的左孩子,且没有叔叔结点(父结点的兄弟结点),此时需要进行两次旋转,第一次,以父结点10右旋:
然后将父结点(此时是9)染为黑色,祖父结点1染为红色,如下所示:
然后以祖父结点1左旋:
下一步,插入元素2,结果如下:
此时情况与上一步类似,区别在于父结点1是祖父结点9的左孩子,当前结点2是父结点的右孩子,且叔叔结点10是红色的。这时需要先将叔叔结点10染为黑色,再进行下一步操作,具体做法是将父结点1和叔叔结点10染为黑色,祖父结点9染为红色,如下所示:
由于结点9是根节点,必须为黑色,将它染为黑色即可:
下一步,插入元素3,如下所示:
这和我们之前插入元素10的情况一模一样,需要将父结点2染为黑色,祖父结点1染为红色,如下所示:
然后左旋:
下一步,插入元素8,结果如下:
此时和插入元素2有些类似,区别在于父结点3是右孩子,当前结点8也是右孩子,这时也需要先将叔叔结点1染为黑色,具体操作是先将1和3染为黑色,再将祖父结点2染为红色,如下所示:
此时树已经平衡了,不需要再进行其他操作了,现在插入元素7,如下所示:
这时和之前插入元素9时一模一样了,先将7和8右旋,如下所示:
然后将7染为黑色,3染为红色,再进行左旋,结果如下:
下一步要插入的元素是4,结果如下:
这里和插入元素2是类似的,先将3和8染为黑色,7染为红色,如下所示: