十二、Map 子接口之 TreeMap
12.1 TreeMap
TreeMap 由红黑树实现,可以保持元素的自然顺序,或者实现了 Comparator 接口的自定义顺序。
红黑树(Red-black tree)是一种自平衡的二叉查找树,结构复杂,但是有着良好的性能,完成查找、插入和删除的时间复杂度均为 log(n)。
二叉查找树,它有几个特点:
- 左子树上所有节点的值均小于或等于它的根节点的值;
- 右子树上所有节点的值均大于或等于它的根节点的值;
- 左右子树也分别为二叉排序树。
但是,二叉查找树有一个不足,就是容易变成瘸子,就是一侧多、一侧少:
这样的话查找的效率就要从 log(n) 变成 O(n) 了,所以还是有必要平衡一下的,这样就有了平衡二叉树,左右两个子树的高度差的绝对值不超过 1:
红黑树就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡:
- 每个节点都只能是红色或者黑色;
- 根节点是黑色;
- 每个叶节点(NIL 节点,空节点)是黑色的;
- 如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点;
- 从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点。
12.2 自然顺序
默认情况下,TreeMap 是根据 key 的自然顺序排列的。比如说整数,就是升序:1、2、3、4、5:
TreeMap<Integer, String> treeMap = new TreeMap<>();
treeMap.put(5, "张三");
treeMap.put(7, "李四");
treeMap.put(3, "王五");
treeMap.put(1, "赵六");
treeMap.put(8, "乔七");
System.out.println(treeMap); // {1=赵六, 3=王五, 5=张三, 7=李四, 8=乔七}
扒一下 TreeMap 中 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;
// 用来进行 key 的比较,用的是什么类型就调用什么类型的 compareTo() 方法
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;
}
什么类型的 key 就会默认调用什么类型的比较方法,cmp = k.compareTo(t.key);
就是用来进行 key 的比较的。由于此时 key 是 int 类型,所以就会调用 Integer 类的 compareTo() 方法进行比较:
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
相应的,如果 key 是字符串的话,就会调用 String 类的 compareTo() 方法进行比较:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
当 key 是 String 类型时,其内部是由字符串的字节数组的字符进行比较的:
TreeMap<String, String> treeMap1 = new TreeMap<>();
treeMap1.put("c", "张三");
treeMap1.put("b", "李四");
treeMap1.put("d", "王五");
treeMap1.put("a", "赵六");
treeMap1.put("f", "乔七");
System.out.println(treeMap1); // {a=赵六, b=李四, c=张三, d=王五, f=乔七}
12.3 自定义排序
如果自然顺序不满足比较的要求,就可以在声明 TreeMap 对象的时候指定排序规则:
TreeMap<Integer, String> treeMap2 = new TreeMap<>(Comparator.reverseOrder());
treeMap2.put(5, "张三");
treeMap2.put(7, "李四");
treeMap2.put(3, "王五");
treeMap2.put(1, "赵六");
treeMap2.put(8, "乔七");
System.out.println(treeMap2); // {8=乔七, 7=李四, 5=张三, 3=王五, 1=赵六}
TreeMap 提供了可以指定排序规则的构造方法:
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
Comparator.reverseOrder()
返回的是 ReverseComparator 对象,就是用来反转顺序的,非常方便。
HashMap 是无序的,插入的顺序随着元素的增加会不停的变动。但是 TreeMap 能够自始至终按照指定的顺序排列。
12.4 排序的好处
既然 TreeMap 的元素是经过排序的,那么找出最大值、最小值、或者找出所有大于或者小于某个值的键来说就方便多了:
Integer lastKey = treeMap.lastKey();
Integer firstKey = treeMap.firstKey();
Integer lowerKey = treeMap.lowerKey(1);
Set<Integer> headKey = treeMap.headMap(3).keySet();
Set<Integer> tailKey = treeMap.tailMap(3).keySet();
System.out.println(lastKey); // 8
System.out.println(firstKey); // 1
System.out.println(lowerKey); // null
System.out.println(headKey); // [1]
System.out.println(tailKey); // [3, 5, 7, 8]
TreeMap 就恰好提供了 lastKey()、firstKey() 这样获取最后一个 key 和第一个 key 的方法。
headMap() 获取的是到指定 key 之前的 key;tailMap() 获取的是指定 key 之后的 key(包括指定 key)。
12.5 如何选择 Map
HashMap、LinkedHashMap、TreeMap 都实现了 Map 接口,并提供了几乎相同的功能(增删改查),它们之间最大的区别就在于元素的顺序:
-
HashMap 完全不保证元素的顺序,添加了新的元素,之前的顺序可能完全逆转。
-
LinkedHashMap 默认会保持元素的插入顺序。
-
TreeMap 默认会保持 key 的自然顺序(根据 compareTo() 方法)。
HashMap | LinkedHashMap | TreeMap | |
---|---|---|---|
排序 | 无序 | 插入顺序 | 自然顺序 |
get/put/remove | O(1) | O(1) | O(log(n)) |
接口 | Map | Map | NavigableMap/Map/SortedMap |
null key/value | 允许 | 允许 | value 允许 |
实现方式 | 桶 | 双向链表桶 | 红黑树 |
红黑树 | 否 | 否 | 否 |