如需转载,请根据 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 许可,附上本文作者及链接。
本文作者: 执笔成念
作者昵称: zbcn
本文链接: https://1363653611.github.io/zbcn.github.io/2020/10/09/JCF_03TreeMap/
总体介绍
Java TreeMap实现了SortedMap接口,也就是说会按照key
的大小顺序对Map中的元素进行排序,key
大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator)。
TreeMap
底层通过红黑树(Red-Black tree)实现,也就意味着containsKey()
, get()
, put()
, remove()
都有着log(n)
的时间复杂度。
出于性能原因,TreeMap是非同步的(not synchronized),如果需要在多线程环境使用,需要程序员手动同步;或者通过如下方式将TreeMap包装成(wrapped)同步的:
1 | SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...)); |
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过1,具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色
- 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
- 对于每个节点,从该点至
null
(树尾端)的任何路径,都含有相同个数的黑色节点。
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查找树重新满足红黑树的条件。
预备知识
前文说到当查找树的结构发生改变时,红黑树的条件可能被破坏,需要通过调整使得查找树重新满足红黑树的条件。调整可以分为两类:一类是颜色调整,即改变某个节点的颜色;另一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操作:左旋(Rotate Left),右旋(RotateRight)。
左旋
左旋的过程是将x
的右子树绕x
逆时针旋转,使得x
的右子树成为x
的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
TreeMap中左旋代码如下
1 | /** From CLR */ |
2 | private void rotateLeft(Entry<K,V> p) { |
3 | if (p != null) { |
4 | Entry<K,V> r = p.right; |
5 | p.right = r.left; |
6 | if (r.left != null) |
7 | r.left.parent = p; |
8 | r.parent = p.parent; |
9 | if (p.parent == null) |
10 | root = r; |
11 | else if (p.parent.left == p) |
12 | p.parent.left = r; |
13 | else |
14 | p.parent.right = r; |
15 | r.left = p; |
16 | p.parent = r; |
17 | } |
18 | } |
右旋
右旋的过程是将x
的左子树绕x
顺时针旋转,使得x
的左子树成为x
的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
TreeMap中右旋代码如下:
1 | /** From CLR */ |
2 | private void rotateRight(Entry<K,V> p) { |
3 | if (p != null) { |
4 | Entry<K,V> l = p.left; |
5 | p.left = l.right; |
6 | if (l.right != null) l.right.parent = p; |
7 | l.parent = p.parent; |
8 | if (p.parent == null) |
9 | root = l; |
10 | else if (p.parent.right == p) |
11 | p.parent.right = l; |
12 | else p.parent.left = l; |
13 | l.right = p; |
14 | p.parent = l; |
15 | } |
16 | } |
方法剖析
get()
get(Object key)
方法根据指定的key
值返回对应的value
,该方法调用了getEntry(Object key)
得到相应的entry
,然后返回entry.value
。因此getEntry()
是算法的核心。算法思想是根据key
的自然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0
的entry
。
具体代码如下:
1 | //getEntry()方法 |
2 | final Entry<K,V> getEntry(Object key) { |
3 | ...... |
4 | if (key == null)//不允许key值为null |
5 | throw new NullPointerException(); |
6 | Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序 |
7 | Entry<K,V> p = root; |
8 | while (p != null) { |
9 | int cmp = k.compareTo(p.key); |
10 | if (cmp < 0)//向左找 |
11 | p = p.left; |
12 | else if (cmp > 0)//向右找 |
13 | p = p.right; |
14 | else |
15 | return p; |
16 | } |
17 | return null; |
put()
put(K key, V value)
方法是将指定的key
, value
对添加到map
里。该方法首先会对map
做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于getEntry()
方法;如果没有找到则会在红黑树中插入新的entry
,如果插入之后破坏了红黑树的约束,还需要进行调整(旋转,改变某些节点的颜色)。
1 | public V put(K key, V value) { |
2 | ...... |
3 | int cmp; |
4 | Entry<K,V> parent; |
5 | if (key == null) |
6 | throw new NullPointerException(); |
7 | Comparable<? super K> k = (Comparable<? super K>) key;//使用元素的自然顺序 |
8 | do { |
9 | parent = t; |
10 | cmp = k.compareTo(t.key); |
11 | if (cmp < 0) t = t.left;//向左找 |
12 | else if (cmp > 0) t = t.right;//向右找 |
13 | else return t.setValue(value); |
14 | } while (t != null); |
15 | Entry<K,V> e = new Entry<>(key, value, parent);//创建并插入新的entry |
16 | if (cmp < 0) parent.left = e; |
17 | else parent.right = e; |
18 | fixAfterInsertion(e);//调整 |
19 | size++; |
20 | return null; |
21 | } |
上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,然后创建新的entry
并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion()
,前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。
调整函数fixAfterInsertion()
的具体代码如下,其中用到了上文中提到的rotateLeft()
和rotateRight()
函数。通过代码我们能够看到,情况2其实是落在情况3内的。情况4~情况6跟前三种情况是对称的,因此图解中并没有画出后三种情况,读者可以参考代码自行理解。
1 | /** From CLR */ |
2 | private void fixAfterInsertion(Entry<K,V> x) { |
3 | x.color = RED; |
4 | while (x != null && x != root && x.parent.color == RED) { |
5 | if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { |
6 | Entry<K,V> y = rightOf(parentOf(parentOf(x))); |
7 | if (colorOf(y) == RED) {//如果y为null,则视为BLACK |
8 | setColor(parentOf(x), BLACK); // 情况1 |
9 | setColor(y, BLACK); // 情况1 |
10 | setColor(parentOf(parentOf(x)), RED); // 情况1 |
11 | x = parentOf(parentOf(x)); // 情况1 |
12 | } else { |
13 | if (x == rightOf(parentOf(x))) { |
14 | x = parentOf(x); // 情况2 |
15 | rotateLeft(x); // 情况2 |
16 | } |
17 | setColor(parentOf(x), BLACK); // 情况3 |
18 | setColor(parentOf(parentOf(x)), RED); // 情况3 |
19 | rotateRight(parentOf(parentOf(x))); // 情况3 |
20 | } |
21 | } else { |
22 | Entry<K,V> y = leftOf(parentOf(parentOf(x))); |
23 | if (colorOf(y) == RED) { |
24 | setColor(parentOf(x), BLACK); // 情况4 |
25 | setColor(y, BLACK); // 情况4 |
26 | setColor(parentOf(parentOf(x)), RED); // 情况4 |
27 | x = parentOf(parentOf(x)); // 情况4 |
28 | } else { |
29 | if (x == leftOf(parentOf(x))) { |
30 | x = parentOf(x); // 情况5 |
31 | rotateRight(x); // 情况5 |
32 | } |
33 | setColor(parentOf(x), BLACK); // 情况6 |
34 | setColor(parentOf(parentOf(x)), RED); // 情况6 |
35 | rotateLeft(parentOf(parentOf(x))); // 情况6 |
36 | } |
37 | } |
38 | } |
39 | root.color = BLACK; |
40 | } |
remove()
remove(Object key)
的作用是删除key
值对应的entry
,该方法首先通过上文中提到的getEntry(Object key)
方法找到key
值对应的entry
,然后调用deleteEntry(Entry<K,V> entry)
删除对应的entry
。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要进行调整。
寻找节点后继
对于一棵二叉查找树,给定节点t,其后继(树种比大于t的最小的那个元素)可以通过如下方式找到:
- t的右子树不空,则t的后继是其右子树中最小的那个元素。
- t的右孩子为空,则t的后继是其第一个向左走的祖先。
后继节点在红黑树的删除操作中将会用到。
TreeMap中寻找节点后继的代码如下:
1 | / 寻找节点后继函数successor() |
2 | static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) { |
3 | if (t == null) |
4 | return null; |
5 | else if (t.right != null) {// 1. t的右子树不空,则t的后继是其右子树中最小的那个元素 |
6 | Entry<K,V> p = t.right; |
7 | while (p.left != null) |
8 | p = p.left; |
9 | return p; |
10 | } else {// 2. t的右孩子为空,则t的后继是其第一个向左走的祖先 |
11 | Entry<K,V> p = t.parent; |
12 | Entry<K,V> ch = t; |
13 | while (p != null && ch == p.right) { |
14 | ch = p; |
15 | p = p.parent; |
16 | } |
17 | return p; |
18 | } |
19 | } |
getEntry()
函数前面已经讲解过,这里重点放deleteEntry()
上,该函数删除指定的entry
并在红黑树的约束被破坏时进行调用fixAfterDeletion(Entry<K,V> x)
进行调整。
由于红黑树是一棵增强版的二叉查找树,红黑树的删除操作跟普通二叉查找树的删除操作也就非常相似,唯一的区别是红黑树在节点删除之后可能需要进行调整。在考虑一棵普通二叉查找树的删除过程,可以简单分为两种情况:
- 删除点p的左右子树都为空,或者只有一棵子树非空。
- 处理起来比较简单,直接将p删除(左右子树都为空时),或者用非空子树替代p(只有一棵子树非空时)
- 删除点p的左右子树都非空。
- 可以用p的后继s(树中大于x的最小的那个元素)代替p,然后使用情况1删除s(此时s一定满足情况1,可以画画看)
deleteEntry()
1 | // 红黑树entry删除函数deleteEntry() |
2 | private void deleteEntry(Entry<K,V> p) { |
3 | modCount++; |
4 | size--; |
5 | if (p.left != null && p.right != null) {// 2. 删除点p的左右子树都非空。 |
6 | Entry<K,V> s = successor(p);// 后继 |
7 | p.key = s.key; |
8 | p.value = s.value; |
9 | p = s; |
10 | } |
11 | Entry<K,V> replacement = (p.left != null ? p.left : p.right); |
12 | if (replacement != null) {// 1. 删除点p只有一棵子树非空。 |
13 | replacement.parent = p.parent; |
14 | if (p.parent == null) |
15 | root = replacement; |
16 | else if (p == p.parent.left) |
17 | p.parent.left = replacement; |
18 | else |
19 | p.parent.right = replacement; |
20 | p.left = p.right = p.parent = null; |
21 | if (p.color == BLACK) |
22 | fixAfterDeletion(replacement);// 调整 |
23 | } else if (p.parent == null) { |
24 | root = null; |
25 | } else { // 1. 删除点p的左右子树都为空 |
26 | if (p.color == BLACK) |
27 | fixAfterDeletion(p);// 调整 |
28 | if (p.parent != null) { |
29 | if (p == p.parent.left) |
30 | p.parent.left = null; |
31 | else if (p == p.parent.right) |
32 | p.parent.right = null; |
33 | p.parent = null; |
34 | } |
35 | } |
36 | } |
上述代码中占据大量代码行的,是用来修改父子节点间引用关系的代码,其逻辑并不难理解。下面着重讲解删除后调整函数fixAfterDeletion()
。首先请思考一下,删除了哪些点才会导致调整?只有删除点是BLACK的时候,才会触发调整函数,因为删除RED节点不会破坏红黑树的任何约束,而删除BLACK节点会破坏规则4。
跟上文中讲过的fixAfterInsertion()
函数一样,这里也要分成若干种情况。记住,无论有多少情况,具体的调整操作只有两种:1.改变某些节点的颜色,2.对某些节点进行旋转。
上述图解的总体思想是:将情况1首先转换成情况2,或者转换成情况3和情况4。当然,该图解并不意味着调整过程一定是从情况1开始。通过后续代码我们还会发现几个有趣的规则:
a).如果是由情况1之后紧接着进入的情况2,那么情况2之后一定会退出循环(因为x为红色);
b).一旦进入情况3和情况4,一定会退出循环(因为x为root)。
删除后调整函数fixAfterDeletion()
的具体代码如下,其中用到了上文中提到的rotateLeft()
和rotateRight()
函数。通过代码我们能够看到,情况3其实是落在情况4内的。情况5~情况8跟前四种情况是对称的,因此图解中并没有画出后四种情况,读者可以参考代码自行理解。
1 | private void fixAfterDeletion(Entry<K,V> x) { |
2 | while (x != root && colorOf(x) == BLACK) { |
3 | if (x == leftOf(parentOf(x))) { |
4 | Entry<K,V> sib = rightOf(parentOf(x)); |
5 | if (colorOf(sib) == RED) { |
6 | setColor(sib, BLACK); // 情况1 |
7 | setColor(parentOf(x), RED); // 情况1 |
8 | rotateLeft(parentOf(x)); // 情况1 |
9 | sib = rightOf(parentOf(x)); // 情况1 |
10 | } |
11 | if (colorOf(leftOf(sib)) == BLACK && |
12 | colorOf(rightOf(sib)) == BLACK) { |
13 | setColor(sib, RED); // 情况2 |
14 | x = parentOf(x); // 情况2 |
15 | } else { |
16 | if (colorOf(rightOf(sib)) == BLACK) { |
17 | setColor(leftOf(sib), BLACK); // 情况3 |
18 | setColor(sib, RED); // 情况3 |
19 | rotateRight(sib); // 情况3 |
20 | sib = rightOf(parentOf(x)); // 情况3 |
21 | } |
22 | setColor(sib, colorOf(parentOf(x))); // 情况4 |
23 | setColor(parentOf(x), BLACK); // 情况4 |
24 | setColor(rightOf(sib), BLACK); // 情况4 |
25 | rotateLeft(parentOf(x)); // 情况4 |
26 | x = root; // 情况4 |
27 | } |
28 | } else { // 跟前四种情况对称 |
29 | Entry<K,V> sib = leftOf(parentOf(x)); |
30 | if (colorOf(sib) == RED) { |
31 | setColor(sib, BLACK); // 情况5 |
32 | setColor(parentOf(x), RED); // 情况5 |
33 | rotateRight(parentOf(x)); // 情况5 |
34 | sib = leftOf(parentOf(x)); // 情况5 |
35 | } |
36 | if (colorOf(rightOf(sib)) == BLACK && |
37 | colorOf(leftOf(sib)) == BLACK) { |
38 | setColor(sib, RED); // 情况6 |
39 | x = parentOf(x); // 情况6 |
40 | } else { |
41 | if (colorOf(leftOf(sib)) == BLACK) { |
42 | setColor(rightOf(sib), BLACK); // 情况7 |
43 | setColor(sib, RED); // 情况7 |
44 | rotateLeft(sib); // 情况7 |
45 | sib = leftOf(parentOf(x)); // 情况7 |
46 | } |
47 | setColor(sib, colorOf(parentOf(x))); // 情况8 |
48 | setColor(parentOf(x), BLACK); // 情况8 |
49 | setColor(leftOf(sib), BLACK); // 情况8 |
50 | rotateRight(parentOf(x)); // 情况8 |
51 | x = root; // 情况8 |
52 | } |
53 | } |
54 | } |
55 | setColor(x, BLACK); |
56 | } |
TreeSet
TreeSet
是对TeeMap
的简单包装,对TreeSet
的函数调用都会转换成合适的TeeMap
方法,因此TreeSet
的实现非常简单。这里不再赘述。
1 | // TreeSet是对TreeMap的简单包装 |
2 | public class TreeSet<E> extends AbstractSet<E> |
3 | implements NavigableSet<E>, Cloneable, java.io.Serializable |
4 | { |
5 | ...... |
6 | private transient NavigableMap<E,Object> m; |
7 | // Dummy value to associate with an Object in the backing Map |
8 | private static final Object PRESENT = new Object(); |
9 | public TreeSet() { |
10 | this.m = new TreeMap<E,Object>();// TreeSet里面有一个TreeMap |
11 | } |
12 | ...... |
13 | public boolean add(E e) { |
14 | return m.put(e, PRESENT)==null; |
15 | } |
16 | ...... |
17 | } |