“ 人生苦短,不如养狗”
这段时间在重新复习一些Java基础知识,看到HashMap在1.8的改进中增加了红黑树,不经产生了一个疑问:为什么是红黑树?同样是二叉树,为什么红黑树能这么优秀?
01—什么是红黑树
红黑树,是一种平衡二叉搜索树。既具有了二叉平衡树的特性,又兼具了二叉搜索树的特性。
在红黑树中,每个结点都关联一个额外的属性:红色或黑色中的一种颜色。同时红黑树做了一下限制:
- 根性质:根节点是黑色的
- 结点性质:每个节点要么是黑色,要么是红色
- 外部性质:每个叶子结点(NIL)是黑色的
- 内部性质:红色结点的孩子结点是黑色的
- 深度性质:对于每个节点,该节点到所有后代叶节点的路径上黑色节点的数目相同
由此,可以得出引理:一棵具有n个节点的红黑树,其树高至多为2log(n 1) 。
下图是一个简单的红黑树。
02—how old are you
经过20多年的发展,Java已经发展到了Java 13,无数的改进和新特性让人目不暇接,学得头秃。就比如红黑树,翻开jdk1.8的集合源码,感觉好像遍地都是红黑树的影子,让人不禁感叹:怎么老是你?
1. HashMap中的红黑树
首先,绕不开的就是大家最熟悉的HashMap。在jdk1.8之前的HashMap中使用的数组 链表的结构,而到了jdk1.8之后,为了改善链表的查询效率,在原有的基础上增加了红黑树。通过下面插入的过程我们就可以看到jdk1.8中的不同之处:
代码语言:javascript复制Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 出现hash冲突的处理:
//(1)如果值也相等,则直接覆盖
//(2)如果该节点属于红黑树,则按照红黑树的方式进行插入
//(3)如果该节点不是红黑树,判断当前链表长度是否达到7,如果达到7则将原链表改为红黑树并插入节点,如果没有达到7则按照链表方式插入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
modCount;
if ( size > threshold)
resize();
afterNodeInsertion(evict);
return null;
在上面的代码中,在处理hash冲突时,当链表的长度大于等于7时,就会将链表改为红黑树。这是因为当链表长度小于8的时候,链表虽然在查询效率(最坏时间复杂度为O(n))上低于红黑树(最坏时间复杂度O(logn)),但是插入和删除操作开销远小于红黑树完成相同操作所需的开销,总体会优于红黑树。而当链表的长度不断增加的时候,查询、插入和删除的开销都在大幅增加。此时红黑树的整体效率会高于链表。
我们再来看一下HashMap中的红黑树是如何实现的:
代码语言:javascript复制static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
可以看到,这个红黑树的结构就如同定义中给出的,有父结点、左孩子结点、右孩子结点以及前驱结点(在进行删除操作的时候需要考虑),还有一个boolean变量表示该节点是否为红。
2. TreeMap中的红黑树
Map中的另一个重要实现类——TreeMap。在源码中是这样描述TreeMap的:
代码语言:javascript复制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.
可以看到TreeMap就是基于红黑树来实现的。同时,TreeMap还是一个按照key值进行自然排列的有序集合。当然,我们也可以指定排序方式。
代码语言:javascript复制/**
* The comparator used to maintain order in this tree map, or
* null if it uses the natural ordering of its keys.
*
* @serial
*/
private final Comparator<? super K> comparator;
private transient Entry<K,V> root;
/**
* The number of entries in the tree
*/
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
其中成员变量comparator就是用于指定排序方式,当然如果不需要指定排序方式,那么在使用构造方法时这个变量就会被设置为null,默认使用自然排序。
其中成员变量root就是红黑树的其中一个结点,我们可以看下具体Entry<K,V>里面是什么结构:
代码语言:javascript复制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;
....
}
主要还是来看下Entry<K, V>的逻辑结构,其实和HashMap中的结构类似,不同之处在于,颜色默认是黑色,并且剔除了前驱结点。
在TreeMap中使用红黑树作为实现逻辑,个人理解应该就是避免了使用纯粹的二叉搜索树出现的问题。当然这也是平衡二叉搜索树出现的原因。
Java中还有许多地方都使用了红黑树这样一个数据结构。这里就不一一列举出来了,大家如果有兴趣的话可以翻看jdk的源码寻找一下。
03—为何你一枝独秀
必须得承认红黑树很优秀,但是同样是提升检索效率,为什么不考虑使用AVL树等其他的平衡二叉搜索树呢?
关键就在于红黑树对于结点着色方式的限制上面。由于存在着对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度<=红黑树)。相对于AVL树的高度平衡而言(HB(k),即其平衡因子为1),红黑树维持平衡所需进行的旋转次数更少。所以对于搜索、插入和删除操作频繁的场景,更适合使用的是红黑树。
04—总结
经过上述的一番探究,不禁感慨,大佬出手必有其缘由。当然,从jdk的变更中我们也可以看到,每一个数据结构都有其适用的范围和特点,我们要根据不同场景使用恰当的数据结构来提升数据处理的性能。