当链表长度大于等于TREEIFY_THRESHOLD(默认8)时,同时链表长度大于等于
MIN_TREEIFY_CAPACITY(默认64)就会触发链表转红黑树的情况,当然,在删除(remove)数据或其它原因调整了大小,当红黑树节点小于或等于6以后,会回复成链表形态。
1、为什么链表要转红黑树
每次遍历链表,平均查询的时间复杂度是O(n),n是链表长度。红黑树和链表的查询性能不一样
由于红黑树自平衡的特点,可以防止不平衡的情况发生,所以红黑树把查询的时间复杂度始
终控制在O(log(n)) 。最初链表不是很长时间,所以O(n)和O(log(n))差距不是很大,随着链表
长度的增加,这种差别就会体现出来,所以需要把链表转为红黑树形式 。
O(n)和O(log(n))的差别:
O(log n)优于O(n)
现在O(log n)到底是什么?
通常,当提到大O表示法时,log n表示以2为底的对数(ln表示底e的对数的方式相同)。 这个以2为底的对数是指数函数的反函数。指数函数快速增长,我们可以直观地推断出它的反函数将恰好相反,即增长非常慢。
例如
x = O(log n)
我们可以将n表示为,
n = 2x
和
210 = 1024→lg(1024)= 10
220 = 1,048,576→lg(1048576)= 20
230 = 1,073,741,824→lg(1073741824)= 30
n的大增量只会导致log(n)的增加很小
也可以参考:图解时间复杂度O(n) - TBHacker - 博客园
2、为什么不一开始就使用红黑树,还要有一个转换的过程
其实在 JDK 的源码注释中已经对这个问题作了解释: (粘贴图)
这段话的意思是:单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间。
3、为什么链表转红黑树的阈(yu 四声)值是8
通过查源码发现,默认是链表长度达到 8 就转成红黑树,而当长度降到 6 就转换回去,这体现了时间和空间平衡的思想,最开始使用链表的时候,空间占用是比较少的,而且由于链表短,所以查询时间也没有太大的问题。可是当链表越来越长,需要用红黑树的形式来保证查询的效率。对于何时应该从链表转化为红黑树,需要确定一个阈值,这个阈值默认为 8,并且在源码中也对选择 8 这个数字做了说明,原文如下:
上面这段话的意思是,如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。
但是,HashMap 决定某一个元素落到哪一个桶里,是和这个对象的 hashCode 有关的,JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,例如:
事实上,链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。
通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。
所以如果平时开发中发现 HashMap 或是 ConcurrentHashMap 内部出现了红黑树的结构,这个时候往往就说明我们的哈希算法出了问题,需要留意是不是我们实现了效果不好的 hashCode 方法,并对此进行改进,以便减少冲突。
4、链表转红黑树代码如下:
treeifyBin()方法:
final void treeifyBin(Node<K,V>[] tab, int hash) {
//n数组长度,index 当前下标,e当前节点
int n, index; Node<K,V> e;
//判断数组是否为null,判断数组长度是否小于64,如果小于走
扩容,不小于走链表转红黑树
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); //扩容
//数组长度减1和hash做与运算的当前下标index,取下标内元素,
如果为null结束,不为null赋值给e
else if ((e = tab[index = (n - 1) & hash]) != null) {
//hd头节点 tl尾节点
TreeNode<K,V> hd = null, tl = null;
do {
//replacementTreeNode 添加接点e 进TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
//判断尾节点 为null 说明没有根节点
if (tl == null)
//首节点(根节点) 指向当前节点
hd = p;
else {
//当前树节点的 前一个节点指向 尾节点
p.prev = tl;
//尾节点的 后一个节点指向 当前节点
tl.next = p;
}
//把当前节点设置为尾节点
tl = p;
} while ((e = e.next) != null); //判断当前节点的下 当前节点的下一个节
点是否为null 不为null 遍历
//目前为止 也只是把Node改为TreeNode 也就是单向链表改为双向链表
//根节点判断是否为 null
if ((tab[index] = hd) != null)
hd.treeify(tab); //转红黑树
}
}
treeify()方法:
参考:链表转红黑树的原因?为什么阈值为8? - JustJavaIt - 博客园 作者:JustJavaIt