HashMap设计思想学习

news2024/10/2 14:33:12

HashMap设计思想学习

  • 引言
  • 树化与退化
  • 红黑树的优势
  • 索引计算
  • put流程
  • 扩容(加载)因子为何默认是 0.75f
  • hashMap并发丢失数据问题
  • jdk 1.7并发死链问题
  • key 的设计


引言

hashmap在jdk 1.7之前是数组+链表结构,而jdk1.8之后变为是数组+(链表|红黑树)


树化与退化

树化意义:

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 O ( 1 ) O(1) O(1),而红黑树的查找,更新的时间复杂度是 O ( l o g 2 ⁡ n ) O(log_2⁡n ) O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left (根节点的左右孩子和左孙子)有一个为 null ,也会退化为链表

总结:

通常情况下,除非是恶意伪造大量hash相同的元素,否则一般情况下链表长度最长也就是6左右,但是为了应对恶意伪造数据进行攻击的情况,引入了红黑树在链表长度达到指定阈值8时,进行替换;当然,如果此时数组长度没有大于等于64,那么会先尝试通过扩容数组大小来减少链表长度。扩容时,如果某个树的元素个数小于了6,那么红黑树会退化为链表,或者红黑树根节点的左右孩子或者左孙子中有一个为null,也会退化为链表。


红黑树的优势

传统的BST二叉搜索树需要满足根节点大于左子树小于右子树的条件,并且查询和插入复杂度为0(logn),但是极端情况下二叉搜索树会退化为线性结构,此时查询和插入复杂度变为o(n)。

AVL自平衡二叉树在二叉搜索树的基础上进行了优化,需要满足左右子树的高度差小于等于1,AVL树的最差查询和插入复杂度也为O(logn)。

AVL自平衡二叉树对"平衡"的定义非常严格,在插入和删除非常频繁的场景下,会产生大量的旋转操作,性能会受到很大影响。

红黑树:

1.每个节点非红即黑

2.root节点必须是黑节点

3.null节点会被视为叶子节点,叶子节点必须是黑节点

4.红节点的子节点必须是黑节点

5.新插入的节点是红节点

6.从任意一个节点出发到叶子节点的任意路径上,黑色节点的个数都是相等的

红黑树和AVL树所要求的严格的"平衡"条件不同,红黑树最大允许左右节点数相差一倍(左子树全部都是黑色节点,右子树一红一黑), 红黑树对"平衡"的条件要求不是那么苛刻,因此红黑树在插入和删除节点时,产生的旋转变换操作会更少,性能更高,并且红黑树整体复杂度依然为0(logn);


索引计算

索引计算方法

  • 首先,计算对象的 hashCode()
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希
    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
  • 最后 & (capacity – 1) 得到索引

hashmap的二次哈希方法中会对key的hashcode进行扰动计算,防止不同的hashcode的高位不同但低位相同导致hash冲突。简单来说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

偶数对偶数进行取模得到的结果还是偶数,因此容量为2的n次幂最大的缺点就是hash分布不均匀,因此,如果追求的是更好的hash分散性,应该采用质数作为数组容量; 但是2的n次幂可以提供很多优化特性,追求性能,还是可以考虑的。


put流程

1.hashMap是懒惰创建数组的,首次使用时才会创建
2.计算索引(桶下标)–>hashcode–>二次哈希–>与运算
3.如果桶下标没人占用,创建Node占位返回
4.如果桶下标已经被占用了
4.1 已经是TreeNode走红黑树添加或者更新逻辑
4.2 是普通的Node,走连接的添加或更新逻辑
4.2.1 如果链表长度超过树化阈值8,并且当前数组容量是小于64,那么会首先通过扩容,减少链表长度
4.2.2 如果链表长度超过树化阈值8,并且当前数组容器是大于等于64,那么会将链表转换为红黑树,走树化逻辑
5.返回前检查容量是否超过扩容阈值,一旦超过进行扩容

jdk 1.7和jdk1.8的不同:

1.插入节点时,jdk1.7采用头插法,1.8采用尾插法
2…1.7是大于等于阈值并且计算出的索引不是空位的情况下才进行扩容,而1.8是大于阈值就扩容
3. 1.7和1.8在在扩容计算Node索引时进行优化,会通过 hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap


扩容(加载)因子为何默认是 0.75f

  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

hashMap并发丢失数据问题

hashMap本身并不是线程安全的,所以无论是jdk 1.7 还是 jdk 1.8,都存在并发丢失数据的风险:
在这里插入图片描述
如果存在两个并发线程1和2都同时向hashmap中put一对键值对,并且key计算出来的hash值都是相同的,那么线程1和线程2同时来到判断索引位是否为空的逻辑,发现为空,填充数据,此时就存在线程2覆盖线程1的数据导致丢失数据的风险。


jdk 1.7并发死链问题

1.7 源码如下:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
  • e 和 next 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

在这里插入图片描述

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

在这里插入图片描述
第一次循环

  • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
  • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
  • 当循环结束是 e 会指向 next 也就是 b 节点

在这里插入图片描述
第二次循环

  • next 指向了节点 a
  • e 头插节点 b
  • 当循环结束时,e 指向 next 也就是节点 a

在这里插入图片描述
第三次循环

  • next 指向了 null
  • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
  • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

在这里插入图片描述
总结: 由于jdk 1.7的hashmap采用的是头插法,所以当存在两个并发线程同时尝试对hash数组进行扩容时,会出现线程1先扩容完毕,将原本的元素A指向B顺序,颠倒为B指向A。而由于线程2无法感知该变化,因此依然按照A指向B的顺序迁移元素到新数组,最终会产生a指向b,b指向a的死链。


key 的设计

key 的设计要求

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然
  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
  3. key 的 hashCode 应该有良好的散列性

如果 key 可变,例如修改了 age 会导致再次查询时查询不到

public class HashMapMutableKey {
    public static void main(String[] args) {
        HashMap<Student, Object> map = new HashMap<>();
        Student stu = new Student("张三", 18);
        map.put(stu, new Object());

        System.out.println(map.get(stu));

        stu.age = 19;
        System.out.println(map.get(stu));
    }

    static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student student = (Student) o;
            return age == student.age && Objects.equals(name, student.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age);
        }
    }
}

String 对象的 hashCode() 设计

  • 目标是达到较为均匀的散列效果,每个字符串的 hashCode 足够独特
  • 字符串中的每个字符都可以表现为一个数字,称为 S i S_i Si,其中 i 的范围是 0 ~ n - 1
  • 散列公式为: S 0 ∗ 3 1 ( n − 1 ) + S 1 ∗ 3 1 ( n − 2 ) + … S i ∗ 3 1 ( n − 1 − i ) + … S ( n − 1 ) ∗ 3 1 0 S_0∗31^{(n-1)}+ S_1∗31^{(n-2)}+ … S_i ∗ 31^{(n-1-i)}+ …S_{(n-1)}∗31^0 S031(n1)+S131(n2)+Si31(n1i)+S(n1)310
  • 31 代入公式有较好的散列特性,并且 31 * h 可以被优化为
    • 即 $32 ∗h -h $
    • 2 5 ∗ h − h 2^5 ∗h -h 25hh
    • h ≪ 5 − h h≪5 -h h5h

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

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

相关文章

【第38天】不同路径数问题 | 网格 dp 入门

本文已收录于专栏&#x1f338;《Java入门一百例》&#x1f338;学习指引序、专栏前言一、网格模型二、【例题1】1、题目描述2、解题思路3、模板代码4、代码解析5.原题链接三、【例题2】1、题目描述2、解题思路3、模板代码4、代码解析5.原题链接三、推荐专栏四、课后习题序、专…

大型物流运输管理系统源码 TMS源码

大型物流运输管理系统源码 TMS是一套适用于物流公司的物流运输管理系统&#xff0c;涵盖物流公司内部从订单->提货->运单->配车->点到->预约->签收->回单->代收货款的全链条管理系统。 菜单功能 一、运营管理 1、订单管理&#xff1a;用于客户意向订…

ChatGPT 到底强大在哪里?(文末有彩蛋)

ChatGPT 是由 OpenAI 开发的一个人工智能聊天机器人程序&#xff0c;于2022年11月推出。该程序使用基于 GPT-3.5 架构的大型语言模型并通过强化学习进行训练。ChatGPT 以文字方式交互&#xff0c;而除了可以通过人类自然对话方式进行交互&#xff0c;还可以用于相对复杂的语言工…

一定要收藏的面试思维导图,粉丝分享面试经验

一位粉丝分享面试经验&#xff1a;1.常见面试题有哪些&#xff1f;主要从以下一些知识点做了准备&#xff1a; 常用的分析方法、Excel、SQL、 A/B测试、产品分析。然后每份面试针对职位要求&#xff0c;还有前期和HR聊天一点点了解这个职位之后&#xff0c;定向准备。 Excel、S…

OpenMMLab 实战营打卡 - 第 7 课

OpenMMLab MMSegmentation内容概要MMSegmentation统一超参MMSegmentation 的项目结构分割模型的模块化设计分割模型的配置文件主干网络的配置ResNet v1c主解码头的配置辅助解码头的配置数据集配置数据处理流水线常用训练策略参考资料内容概要 • MMSegmentation 项目概述 • M…

TSDF算法应用与源码详解

数据与代码链接见文末 1.TSDF整体概述 (1)需要准备的输入 1.原始图片;2.对应的深度信息;3.每张图的相机位姿;4.相机内参 原始输入图像数据(就是一个场景多个视角拍摄的结果) 输入图像的深度信息,位姿信息等(一般由相机得到,也可以通过算法得到) (2)整体概述…

【DFS并查集】岛屿数量

经典的dfs/bfs问题&#xff0c;给一个起点开始搜索&#xff0c;满足条件则继续调用dfs/bfs 从没有访问过的某个陆地出发&#xff0c;将所有能到达的陆地的状态都记录为已访问。下次出发不从已访问的陆地出发&#xff0c;每次出发前都把岛屿数 1即可 class Solution { public…

STM32开发(9)----CubeMX配置外部中断

CubeMX配置外部中断前言一、什么是中断1.STM32中断架构体系2.外部中断/事件控制器&#xff08;EXTI&#xff09;3.嵌套向量中断控制器&#xff08;NIVC&#xff09;二、实验过程1.CubeMX配置2.代码实现3.硬件连接4.实验结果总结前言 本章介绍使用STM32CubeMX对引脚的外部中断进…

MySQL 3:MySQL数据库基本操作 DQL

数据库管理系统的一个重要功能是数据查询。数据查询不应简单地返回数据库中存储的数据&#xff0c;还应根据需要对数据进行过滤&#xff0c;确定数据的显示格式。MySQL 提供了强大而灵活的语句来实现这些操作。MySQL数据库使用select语句查询数据。 select [all|distinct]<…

09- 机器学习经典流程 (中国人寿保费项目) (机器学习)

删除特征: data data.drop([region, sex], axis1)特征数据调整: data.apply( ) # 体重指数&#xff0c;离散化转换&#xff0c;体重两种情况&#xff1a;标准、肥胖 def convert(df,bmi):df[bmi] fat if df[bmi] > bmi else standardreturn df data data.apply(convert, …

EXCEL-职业版本(1)

EXCEL职业版本(1) 工作表 插入 注&#xff1a;默认会在鼠标选中的sheet后面新增&#xff0c;例如图中&#xff0c;当前选中的是sheet2&#xff0c;点击新增后会在sheet2后面自动新增一个sheet 删除 移动或者复制 类似于copy一整个sheet的所有内容 step1 右击【sheet名称】选…

Cordova

一、简介 Cordova 是用 Web 技术&#xff08; HTML&#xff0c;CSS 和 JS &#xff09;构建移动应用的平台。我们可以认为Cordova 是一个容器&#xff0c;用于将的 Web 应用移植到移动端&#xff0c;同时支持移动端的功能&#xff08;例如&#xff1a;定位、蓝牙、摄像头等&am…

Linux内核并发与竞争-原子操作

一.原子操作的概念首先看一下原子操作&#xff0c;原子操作就是指不能再进一步分割的操作&#xff0c;一般原子操作用于变量或者位操作。假如现在要对无符号整形变量 a 赋值&#xff0c;值为 3&#xff0c;对于 C 语言来讲很简单&#xff0c;直接就是&#xff1a; a3但是 C 语言…

机器学习基本原理总结

本文大部分内容参考《深度学习》书籍&#xff0c;从中抽取重要的知识点&#xff0c;并对部分概念和原理加以自己的总结&#xff0c;适合当作原书的补充资料阅读&#xff0c;也可当作快速阅览机器学习原理基础知识的参考资料。 前言 深度学习是机器学习的一个特定分支。我们要想…

Elasticsearch bucket_script、bucket_selector、bucket_sort 区别和应用场景?

1、实战问题POST test-002/_bulk {"index":{"_id":1}} {"name": "张三","city": "beijing"} {"index":{"_id":2}} {"name": "李四","city": "beijing&qu…

简信CRM:医疗健康行业数字化解决方案

随着社会群体健康意识提升、用户消费习惯变化、新冠疫情冲击等因素&#xff0c;人们对于个人和公共安全健康问题就越发重视&#xff0c;而且已经逐渐从对“病”的被动治疗转变为对“健康”的主动管理&#xff0c;医疗健康行业呈现出一片火热的趋势。但医疗健康行业的情况比较复…

idekCTF 2022 比赛复现

Readme 首先 []byte 是 go 语言里面的一个索引&#xff0c;比如&#xff1a; package mainimport "fmt"func main() {var str string "hello"var randomData []byte []byte(str)fmt.Println(randomData[0:]) //[104 101 108 108 111] }上面这串代码会从…

Java程序运行机制

Java语言既具有编译型语言的特征&#xff0c;又具有解释型语言的特征&#xff0c;Java程序要经过先编译后解释两个阶段。高级语言的运行机制&#x1f4cd;编译型语言使用专门的编译器&#xff0c;针对特定的平台&#xff08;移植性差&#xff09;&#xff0c;将高级语言的源代码…

情人节有哪些数码好物值得送礼?情人节实用性强的数码好物推荐

转瞬间&#xff0c;情人节快到了&#xff0c;大家还在为送什么礼物而烦恼&#xff1f;在这个以科技为主的时代&#xff0c;人们正在享受着科技带来的便利&#xff0c;其中&#xff0c;数码产品也成为了日常生活中必不可少的存在。接下来&#xff0c;我来给大家推荐几款比较实用…

大数据框架之Hadoop:入门(四)Hadoop运行模式

Hadoop运行模式包括&#xff1a;本地模式、伪分布式模式以及完全分布式模式。 Hadoop官方网站&#xff1a;http://hadoop.apache.org/ 4.1本地运行模式 4.1.1官方Grep案例 1.创建在hadoop文件夹下面创建一个input文件夹 [roothdp101 hadoop]# mkdir input2.将Hadoop的xml配…