HashMap之 链表转红黑树

当链表长度大于等于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

  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值