hashmap为什么用红黑树_要看HashMap源码,先来看看它的设计思想

HashMap 是日常开发中,用的最多的集合类之一,也是面试中经常被问到的 Java 类之一。同时,HashMap 在实现方式上面又有十分典型的范例。不管是从哪一方面来看,学习 HashMap 都可以说是有利无害的。

分析 HashMap 的源码的文章在网上面已经数不胜数了,本文另辟蹊径来分析 HashMap 的设计思想。

底层数据结构

说到 HashMap 的数据库,我们需要从两个 JDK 版本来分析:JDK7 和 JDK8。

JDK7 版本的 HashMap 的数据结构为:数组 + 链表。而 JDK8 版本的 HashMap 的数据结构为: 数组 + 链表 + 红黑树。可以看到 7 和 8 中 HashMap 的底层数据结构最主要的区别就是 Java8 多了红黑树。

为何是数组加链表?

上文中说到了 不管是 7 或者8 ,底层数据结构都是 数组 + 链表,但这又是为什么呢?

数组是一个链式数据结构。put的时候,通过哈希函数将数据进行 哈希运算 之后,就得到数组的下标,这样子就可以将数据保存在对应的槽中,这个槽在 HashMap 中被称为 Entry。在 get 时候,通过相同的哈希函数,将 key 进行哈希运算,可以得到对应的下标,就可以快速找到该 key 对应的 value。这时候, get 的时间复杂度还是 O(1)。

但,哈希运算就避免不了有哈希冲突,也就说,不同的值通过哈希运算之后可能得到同一个值。在散列表的相关概念中,我们说了几种解决哈希冲突的方案,在 HashMap中,则是采用了链表法。

也就是说,发生了冲突之后,我们在Entry中形成一个单链表。但是这里有存在了一个问题,如果链表过长,检索起来的效率同样也会很低。于是,在 Java8 中,通过链表转红黑树来解决这个问题。

为何要加上红黑树

为什么要链表转红黑树,我们需要从数据结构来解析。

如果从一个无序单链表中检索数据,我们只能从头到尾一个一个检索,一旦数据量很大的情况下,检索的效率就很低。这时,我们想到了红黑树,从目前的情况来看,红黑树能很好地解决这个问题。

我们先来看看红黑树的定义:

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  1. 节点是红色或黑色。
  2. 根是黑色。
  3. 所有叶子都是黑色(叶子是NIL节点)。
  4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

7693c69fbdd0b4f679df6f50c2baa99d.png

红黑树

要是红黑树,首先得是二叉查找树:

二叉查找树(英语:Binary Search Tree),也称为二叉搜索树有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;
  3. 任意节点的左、右子树也分别为二叉查找树;

简单做一个总结,红黑树的左节点要比父节点小,右节点要比父节点大。如果要检索一个数字,可以将时间复杂度从 O(n) 降低到 O(logn)。

当然了,添加了红黑树的数据结构之后,代码实现要比 只用数组 + 链表要复杂了好几倍。看代码的时候简直是不能再痛苦了。

什么时候转成红黑树,有什么转成链表

在源码中有这么一个字段,static final int TREEIFY_THRESHOLD = 8;,见字知义,这个字段的意思链表转红黑树的阈值,也就是 8。同样的,还有这么一个字段,static final int UNTREEIFY_THRESHOLD = 6;,它意思是红黑树转链表的阈值。

为什么是 8 呢?在源码的注释中也有解释,英文翻译过来就是下面的意思。

链表查询的时间复杂度是 O (n),红黑树的查询复杂度是 O (log n)。在链表数据不多的时候,使用链表进行遍历也比较快,只有当链表数据比较多的时候,才会转化成红黑树,但红黑树需要的占用空间是链表的 2 倍,考虑到转化时间和空间损耗,所以我们需要定义出转化的边界值。

在考虑设计 8 这个值的时候,我们参考了泊松分布概率函数,由泊松分布中得出结论,链表各个长度的命中概率为:

* 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
复制代码

意思是,当链表的长度是 8 的时候,出现的概率是 0.00000006,不到千万分之一,所以说正常情况下,链表的长度不可能到达 8 ,而一旦到达 8 时,肯定是 hash 算法出了问题,所以在这种情况下,为了让 HashMap 仍然有较高的查询性能,所以让链表转化成红黑树,我们正常写代码,使用 HashMap 时,几乎不会碰到链表转化成红黑树的情况,毕竟概念只有千万分之一。

为什么两个阈值不一样的,大家想想,如果一样的,在链表达到8 的时候,会转成红黑树,但红黑树转链表的阈值也是8,这时候就会出现循环转换。

链表转红黑树还有一个条件,就是当数组容量大于 64 时,链表才会转化成红黑树

扩容的条件

在说扩容之前,先来说说 HashMap 在 7 和 8 中初始化时的不同表现。

在 Java 7 中,HashMap 初始化的时候,会有个默认容量 (16)。但在 Java8 中,HashMap 初始化的时候,默认容量为0,只有在第一次 put 的时候,才会扩容到 16。(其实 ArrayList 在 Java8 也是这么表现的)。

在 HashMap 源码中,有一个字段定义 static final float DEFAULT_LOAD_FACTOR = 0.75f;。这个字段的意思是,当HashMap 的长度 = HashMap 当前容量 * 0.75的时候,就会发生扩容。

关于为什么负载因子是 0.75,我们可以在源码注释找到一定的答案。

f1c9f77aa302f63edbe98b0e1624dcbf.png

load factor

大致意思就是说负载因子是0.75的时候,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

HashMap的扩容是变成原先容量的 2 倍。

Hash函数

我们先来看看 Java 8 的 hash 函数。

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
复制代码

这里的大概意思就是,先计算出 key 的 hashCode h。然后计算计算 h ^ (h >>> 16)。无符号右移16位。这么做的好处是使大多数场景下,算出来的 hash 值比较分散。

一般来说,hash 值算出来之后,要计算当前 key 在数组中的索引下标位置时,可以采用取模的方式,就是索引下标位置 = hash 值 % 数组大小,这样做的好处,就是可以保证计算出来的索引下标值可以均匀的分布在数组的各个索引位置上,但取模操作对于处理器的计算是比较慢的,数学上有个公式,当 b 是 2 的幂次方时,a % b = a &(b-1),所以此处索引位置的计算公式我们可以更换为: (n-1) & hash。

此问题可以延伸出三个小问题:

1:为什么不用 key % 数组大小,而是需要用 key 的 hash 值 % 数组大小。

答:如果 key 是数字,直接用 key % 数组大小是完全没有问题的,但我们的 key 还有可能是字符串,是复杂对象,这时候用 字符串或复杂对象 % 数组大小是不行的,所以需要先计算出 key 的 hash 值。

2:计算 hash 值时,为什么需要右移 16 位?

答:hash 算法是 h ^ (h >>> 16),为了使计算出的 hash 值更分散,所以选择先将 h 无符号右移 16 位,然后再于 h 异或时,就能达到 h 的高 16 位和低 16 位都能参与计算,减少了碰撞的可能性。

3:为什么把取模操作换成了 & 操作?

答:key.hashCode() 算出来的 hash 值还不是数组的索引下标,为了随机的计算出索引的下表位置,我们还会用 hash 值和数组大小进行取模,这样子计算出来的索引下标比较均匀分布。

取模操作处理器计算比较慢,处理器对 & 操作就比较擅长,换成了 & 操作,是有数学上证明的支撑,为了提高了处理器处理的速度。

hash 冲突时怎么办?

hash 冲突指的是 key 值的 hashcode 计算相同,但 key 值不同的情况。

如果桶中元素原本只有一个或已经是链表了,新增元素直接追加到链表尾部;

如果桶中元素已经是链表,并且链表个数大于等于 8 时,此时有两种情况:

  1. 如果此时数组大小小于 64,数组再次扩容,链表不会转化成红黑树;
  2. 如果数组大小大于 64 时,链表就会转化成红黑树。

这里不仅仅判断链表个数大于等于 8,还判断了数组大小,数组容量小于 64 没有立即转化的原因,猜测主要是因为红黑树占用的空间比链表大很多,转化也比较耗时,所以数组容量小的情况下冲突严重,我们可以先尝试扩容,看看能否通过扩容来解决冲突的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/365710.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实现CA和证书申请

文字说明 1 在CA上执行,建立CA cd /etc/pki/CA touch index.txt echo 0F > serial (umask 077;openssl genrsa -out private/cakey.pem 2048) openssl req -new -x509 -key private/cakey.pem -out cacert.pem -days 3650 填写多项内容:国家&#xff…

端口如何支持非localhost访问_新特性解读 | MySQL 8.0.19 支持 DNS SRV

转载自公众号:玩转MySQL作者:洪斌MySQL Router 是 InnoDB Cluster 架构的访问入口,在架构部署上,官方给出的建议是 router 与应用端绑定部署,避免 router 单点问题。之前还有客户咨询,能否 router 不与应用…

记录奥运-当今五大Java记录框架之间的竞赛

开发人员:Takipi会告诉您何时新代码在生产中中断– Log4J vs SLF4J简单vs Logback vs Java Util日志记录vs LOG4J2 日志记录实际上是每个服务器端应用程序中古老而固有的部分。 这是应用程序以持久且可读的方式输出实时状态的主要方法。 某些应用程序每天可能仅记录…

移动端实现元素拖拽效果插件_基于自然流布局的可视化拖拽搭建平台设计方案...

LowCode 是高效、高性能的拖拽式低代码开发平台. 也是笔者最近一直在研究的方向, 对于可视化搭建平台的实现方案笔者之前写过很多文章, 这里带大家探索一个新方向——基于自然流布局的可视化搭建平台.在我们之前实现的 h5-dooring 搭建平台中, 我们采用了网格布局的方式来实现拖…

07-数据类型

【转】07-数据类型 介绍 存储引擎决定了表的类型,而表内存放的数据也要有不同的类型,每种数据类型都有自己的宽度,但宽度是可选的 详细参考链接:http://www.runoob.com/mysql/mysql-data-types.html mysql常用数据类型概括&#x…

yii::$app-mongodb 查询纪录数_老詹总决赛有多强?12项数据领先乔丹科比,已握10项数据纪录...

勒布朗詹姆贡献了38分16个篮板和10个助攻的狂暴三双数据,并率领湖人淘汰了掘金,这使得他迈进了职业生涯第10次总决赛舞台。我们都知道,詹姆斯几乎统治着NBA季后赛大部分数据纪录,事实上,他在总决赛同样如此。根据《sta…

前端共享桌面_2020 前端学习路线总结,哎呦,不错哦!

2020 前端学习路线总结在 GitHub 看到一个很不错的前端学习路线图(roadmap),从前端基础到前端工程化,再到跨端,都有知识点的覆盖,非常推荐阅读。图下面是我翻译的一个文字版,可以先看图再看文字…

Confluence 6 高级性能诊断

请在你的系统服务请求中包括下面所有的信息,如果可能的话,你也可以在请求中包括你认为最有可能出现的问题。这样的话,可以避免我们进一步对你系统的问题进行询问。 系统信息 Confluence 服务器 你系统信息的屏幕截图 Confluences Administrat…

RequireJS使用注意地方

使用RequireJS做异步模块加载,有几点值得注意的地方: 1.模块定义两种写法 1. 存在依赖的函数式定义 如果模块存在依赖:则第一个参数是依赖的名称数组;第二个参数是函数,在模块的所有依赖加载完毕后,该函…

WildFly上具有AngularJS的Java EE 7和Java WebSocket API(JSR 356)

这篇博客文章描述了用于WebSocket协议的Java API(JSR 356) (这是Java EE 7平台的四个最新JSR之一),并提供了部署在WildFly 8上并可以在OpenShift上在线获得的具体应用程序。 [FR]版本的法语( HTML或PDF &a…

日期加减加1天_2小时整理了13个时间日期函数,动图演示简单易学,收藏备用吧...

Hello,大家好,今天跟大家整理汇总了13个工作中经常用到的日期与时间函数的使用方法,学会它们几乎可以解决所有工作中遇到的,关于日期与时间提取与转换的问题。话不多说,让我们直接开始吧一、了解时间与日期的本质工作中…

点云数据显示_vispy 显示 kitti 点云数据

国内博客找了一圈,居然没有发现有用 vispy 做可视化的代码,这里做一个简单的示例,代码大部分来自官方。import numpy as np import vispy.scene from vispy.scene import visuals import sys# Make a canvas and add simple view canvas vis…

Webpack 常用命令总结以及常用打包压缩方法

前言:Webpack是一款基于node的前端打包工具,它可以将很多静态文件打包起来,自动处理依赖关系后,生成一个.js文件,然后让html来引用,不仅可以做到按需加载,而且可以减少HTTP请求,节约…

CSS3 Filter详解(改变模糊度 亮度 透明度等方法)

文章目录 1.模糊2.灰度3.亮度4.对比度5.饱和度6.色相旋转7.反色8.阴影9.透明度10.褐色CSS3 Filter(滤镜)属性提供了提供模糊和改变元素颜色的功能。CSS3 Fitler 常用于调整图像的渲染、背景或边框显示效果。 -webkit-filter是css3的一个属性,…

laravel中使用offsetSet

首先不用offsetSet方法,使用laravel的硬添加属性如下: 下边使用offsetSet 转载于:https://www.cnblogs.com/qaing123/p/9672241.html

转3d视图快捷键_最全Solidworks快捷键,值得收藏!

SOLIDWORKS软件提供了很多实用的快捷键,如果我们熟练掌握这些快捷键无疑可以减轻工作强度和提高工作效率。当然我们也可以点击【工具】-【自定义】-【键盘】,自己定义一些快捷键。本文为大家整理一些常用的快捷键,多使用快捷键可以帮助我们节…

构建前端自动化工作流环境

目标:建一个自动化工作流环境 自动编译 自动合并 自动刷新 自动部署 工作流: 1 全局安装webpack 执行命令: npm install webpack webpack-cli -g 其中webpack-cil 是命令接口工具 2 初始化当前项目:npm init 然后一路回车…

大型布线:Java云应用程序缺少的技术

您是否曾经想过,为什么大多数Java框架中的依赖项注入仅用于本地进程内服务而不是分布式服务? 我最近在2013年EMC世界大会上遇到了Paul Maritz的主题演讲 (跳至第32分钟),这使我在云平台的背景下思考了这个问题。 主题…

java cpu过高排查_涨薪秘籍:JAVA项目排查cpu负载过高

背景我负责的其中一个项目在空负载的情况下,CPU占用率依然保持着100%左右,线上、测试、开发的服务都一样;是什么导致的呢?在开发环境我查看了请求流量,这个流量可以忽略但CPU占用率一直在60%-100%之间浮动。分析问题流…

巧用css的border属性完成对图片编辑功能的性能优化

一、需求场景: 最近闲来无事,boss提出了一个要求,研究相关代码并完成一个关于编辑图片功能的性能优化,该功能的主要界面展示如下: 通过了几分钟的短暂试用,发现就是一个简单的裁剪并保存用户选择并上传的图…