HashMap第3讲——JDK1.8红黑树细节

2024-06-13 12:04

本文主要是介绍HashMap第3讲——JDK1.8红黑树细节,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上篇文章对HashMap的put方法进行了源码解析,并介绍了其中的两个亮点设计——位运算取代%和扰动计算。其中还有几个细节,比如每次扩容都是2^n是怎么做到的、JDK1.8增加的红黑树结构,由于篇幅原因没有介绍,本节就先来介绍其中的一个细节——红黑树。

一、JDK 1.8为什么增加树结构

上节也提到过,HashMap是通过链地址法解决哈希冲突的,也就是当发生冲突时,新的元素会挂到当前桶的链表中。

这样就会有一个问题,当hash冲突过多,链表就会挂的很长,我们知道,链表越长,它的查询性能就无限趋于O(N),这样显然是不能接收的。

虽然JDK 1.8和1.7的扰动计算,尽可能地使key分散开来,但是效果并不理想,所以当链表过长的时候,就只能对其数据结构进行修改来增加效率。

毫无疑问,最先想到的就是树结构,那么树结构有很多选择,为什么最终选择了红黑树呢?

二、为什么选红黑树

2.1 二查查找树

关于查询效率,我们首先想到的应该使二叉查找树,它的特点就是左节点<root节点<右节点,这样遍历的复杂度就从链表的O(N)变为了O(logN)

但是,但在极端情况下二查查找树会退化为链表,当子节点都比父节点都大或都小时

 

2.2 平衡二叉树(AVL树)

二查查找树在最差的情况下竟然和顺序查找效率相当,这是无法接受的。我们再来看看平衡二叉树。

AVL是严格平衡的二叉树(平衡因子不超过1),每一次插入数据会检查每个节点的左子树和右子树的高度差,如果大于1,就需要进行左旋或右旋操作,因此查询效率最好,最坏的情况都是O(logN)。

不过这是有代价的:

  • 插入操作:由于AVL必须严格保证平衡,那么每次插入数据,如果平衡因子大于1,就必须进行旋转操作,事实上每次插入操作最多只需要旋转1次,所以插入操作的代价仍是O(logN)。

  • 删除操作:AVL删除可以参考二叉树的删除,但是删除之后必须检查从删除节点开始到根节点路径上的所有结点的平衡因子,这样代价就比较大了,每次删除最多需要O(logN)次旋转,因此删除操作的复杂度为O(2logN)。

综上可以发现,对于那些频繁删除和插入的场景,AVL树显然不合适,所以就引入了红黑树。

2.3 红黑树

平衡二叉树严格平衡的策略是以牺牲插入和删除操作为代价,换来的查询效率(O(logN)。是否能找一个这种的策略,既不会让插入和删除操作牺牲太大的代价,又能把查询的效率稳定在O(logN)呢?

当然有,那就是红黑树,先来看下它的特点:

  • 每个节点不是黑色就是红色,根节点永远是黑色。

  • 所有的叶子结点都是黑色的空节点,也就是叶子节点不存数据。

  • 当一个节点是红色,那么它的子节点必定是黑色的。

  • 一个节点的所有子孙节点到该节点的所有路径上,包含相同数目的黑色节点。

如下图:

我们来对比下AVL树:

  • 查询操作:红黑树不像AVL一样是严格平衡的,但查找的效率基本可以维持在O(logN),最差的情况下(最长路径是最短路径的2n-1),比AVL逊色。

  • 插入操作:插入节点时,需要旋转和变色操作。但只需要保证基本平衡就可以了,因此插入节点最多只需要2次旋转,虽然变色需要O(logN),但变色操作十分简单,代价很小。

  • 删除操作:在删除上就比AVL好多了,删除一个节点最多只需要3次旋转操作。

综上,这就是HashMap选择红黑树的原因。

三、红黑树和链表的转换时机

OK,我们回到代码来,通过上一节我们知道,数组长度大于64&&链表长度大于8,会转为红黑树,我们背过八股文的都知道,当红黑树节点小于等于6时又会转为链表,那么这些临界值是怎样考量的呢?

3.1 红黑树转链表的时机

为什么组长度大于64 && 链表长度大于8,才转为红黑树呢?

ps:首先明确一点,红黑树占用的空间是链表的2倍。

我们知道当数组长度小于64时,不会转为红黑树,而是会选择扩容,这也不难想到,因为数组长度小于64时,说明目前的数据量比较小,没必要转为红黑树。

而链表长度大于8呢?我们先来看一段源码注释:

/* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins.  In
* usages with well-distributed user hashCodes, tree bins are
* rarely used.  Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006
* more: less than 1 in ten million      
*/

大概意思就是:

理想情况下,使用随机的哈希码,节点分布在hash桶中的频率遵循泊松分布,按照泊松分布的公式计算,因为哈希冲突造成桶的链表长度为8时的概率只有0.00000006,这个概率足够低了,并且到8个节点时,红黑树的性能优势也会开始展现出来,因此8是一个比较合理的数字。

3.2 链表转红黑树的时机

对于同一个索引位置,当红黑树节点小于等于6时,就会触发红黑树转链表。(这个扩容的时候会讲)。

经过3.1小节的介绍,这也不难理解,主要原因还是对于性能和空间的考虑,那为什么不能在小于8的时候立刻转会来呢?

如果8的时候转红黑树,小于8就立刻转回来,那么这就可能导致频繁的转换,所以要选择一个小于8的临界值,但又不能是7,从前面的泊松分布可以看到,当红黑树节点小于6时,它所带来的优势其实已经没有那么大了,就不足以抵消由于红黑树维护节点所带来的额外开销,此时转换回来能节省空间和时间。

四、红黑树的双向链表

看过源码的同学会发现,HashMap红黑树的数据结构中,不仅有常见的parent、left、right节点,还有一个nextprev节点。这说明它不仅是一个红黑树,还是一个双向链表。

这样做的原因:

红黑树会记录树化之前的链表结构,这样当红黑树退化成链表的时候,就可以直接按照链表重新链接的方式进行。

不过可能有人会问,那不是需要一个next节点就行了,为啥还要prev呢?

如果删除的是原始链表的中间节点,只靠next是无法将原始的链表重新连接的,所以就需要有prev节点,找到上一个节点,重新连接。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

这篇关于HashMap第3讲——JDK1.8红黑树细节的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1057246

相关文章

Java遍历HashMap的6种常见方式

《Java遍历HashMap的6种常见方式》这篇文章主要给大家介绍了关于Java遍历HashMap的6种常见方式,方法包括使用keySet()、entrySet()、forEach()、迭代器以及分别... 目录1,使用 keySet() 遍历键,再通过键获取值2,使用 entrySet() 遍历键值对3,

Golang HashMap实现原理解析

《GolangHashMap实现原理解析》HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持高效的插入、查找和删除操作,:本文主要介绍GolangH... 目录HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持

JAVA保证HashMap线程安全的几种方式

《JAVA保证HashMap线程安全的几种方式》HashMap是线程不安全的,这意味着如果多个线程并发地访问和修改同一个HashMap实例,可能会导致数据不一致和其他线程安全问题,本文主要介绍了JAV... 目录1. 使用 Collections.synchronizedMap2. 使用 Concurren

Centos7安装JDK1.8保姆版

工欲善其事,必先利其器。这句话同样适用于学习Java编程。在开始Java的学习旅程之前,我们必须首先配置好适合的开发环境。 通过事先准备好这些工具和配置,我们可以避免在学习过程中遇到因环境问题导致的代码异常或错误。一个稳定、高效的开发环境能够让我们更加专注于代码的学习和编写,提升学习效率,减少不必要的困扰和挫折感。因此,在学习Java之初,投入一些时间和精力来配置好开发环境是非常值得的。这将为我

vscode中使用go环境配置细节

1、在docker容器中下载了go的sdk 2、在/etc/profile.d/go.sh里填入如下内容: #!/bin/bashexport GOROOT=/home/ud_dev/goexport PATH=$GOROOT/bin:$PATH  3、设置go env go env -w GOPROXY=https://goproxy.cn,directgo env -w GO

使用WebP解决网站加载速度问题,这些细节你需要了解

说到网页的图片格式,大家最常想到的可能是JPEG、PNG,毕竟这些老牌格式陪伴我们这么多年。然而,近几年,有一个格式悄悄崭露头角,那就是WebP。很多人可能听说过,但到底它好在哪?你的网站或者项目是不是也应该用WebP呢?别着急,今天咱们就来好好聊聊WebP这个图片格式的前世今生,以及它值不值得你花时间去用。 为什么会有WebP? 你有没有遇到过这样的情况?网页加载特别慢,尤其是那

分享MSSQL、MySql、Oracle的大数据批量导入方法及编程手法细节

1:MSSQL SQL语法篇: BULK INSERT      [ database_name . [ schema_name ] . | schema_name . ] [ table_name | view_name ]         FROM 'data_file'        [ WITH       (      [ [ , ] BATCHSIZE = batch_siz

【LVI-SAM】激光雷达点云处理特征提取LIO-SAM 之FeatureExtraction实现细节

激光雷达点云处理特征提取LIO-SAM 之FeatureExtraction实现细节 1. 特征提取实现过程总结1.0 特征提取过程小结1.1 类 `FeatureExtraction` 的整体结构与作用1.2 详细特征提取的过程1. 平滑度计算(`calculateSmoothness()`)2. 标记遮挡点(`markOccludedPoints()`)3. 特征提取(`extractF

HashMap中常用的函数

假设如下HashMap<String, Integer> map = new HashMap<>();获取value值1、返回key为a的valueget(a)2、返回key为a的value,若没有该key返回0getOrDefault(a,0)新增键值对1、新增键值对(a,1)put(a,1)2、如果key为a的键不存在,则存入键值对(a,1)putIfAbsent(a,1)3

hashmap的存值,各种遍历方法

package com.jefflee;import java.util.HashMap;import java.util.Iterator;import java.util.Map;public class HashmapTest {// 遍历Hashmap的四种方法public static void main(String[] args) {//hashmap可以存一个null,把