HashMap

前言

本篇博客将根据现有知识对数据结构HashMap做以小结,以下博客仅作为个人学习过程的小结,如能对各位博友有所帮助不胜荣幸。
本篇博客将简单介绍哈希表和HashMap的概念原理应用,以及各自特点,本篇只做本人小结,后期随学习深入再做补充修改。

哈希表

概念及背景

在顺序表和平衡树中查找元素时,每次查找一个元素都要遍历整个集合进行多次的元素比较,其效率由其比较的次数决定,顺序表中查找的时间复杂度为O(n),平衡树中为O(log(n))

而理想的查找方式是,创建一种数据结构,通过某用函数计算给出一个元素的计算值,再将这些计算值与元素在集合中存储的位置之间建立映射关系,使得查找一个元素时,通过函数的计算值可以直接找到其存储位置,不许要经过任何比较,如此一来查找的效率就有了大幅的提高

上述提到的这种方法叫做哈希方法,其中用到计算位置的函数叫做哈希函数,用这种方法创建出来的数据结构就叫作哈希表
在这里插入图片描述

哈希冲突

由于哈希函数是确定的,有可能经过哈希函数的计算,使得本不相等的两个数,哈希地址是相同的,而存储的位置只有一个,此时就出现了哈希冲突

尽量避免哈希冲突

首先需要明白一点,由于提供的存储空间是有限的,而当要存储的元素个数大于空间中可存放个数时,就必然会发生哈希冲突,因此在解决哈希冲突时我们只能尽量降低冲突的发生,却无法杜绝

降低哈希冲突发生概率的一大途径就是通过完善,设计合理的哈希函数

哈希函数的设计原则:1、哈希函数的定义域必须包含所有需要存储的元素值,而且如果存储空间允许有n个地址,则哈希函数的值域必须在0~n-1之间。2、其计算出来的地址必须能均匀分布在整个空间

常用的哈希函数:

  • 直接定制:取元素值的某个线性关系为存储地址,Hash(key) = A*Key+B优点简单、均匀,适用场景:查询数据量较少且连续的情况
  • 除留余数:当地址数为m,取一个不大于m,但最接近m的质数p,让元素值对p去余,Hash(key) = key%p,此方法最为常用
  • 随机数法:直接调用Random类下发方,创建随机数,Hash(key) = Random(key)

    哈希函数设计越精妙,产生哈希冲突的概率就越低,但始终无法避免

Hash冲突的解决办法

Hash冲突的解决办法有两种:闭散列和开散列

闭散列法也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的“下一个” 空位置中,而寻找空的位置又有两种方法

  1. 线性探针法,即在发生冲突的位置,从此位置开始向后探测,直到探测到空位置将元素法入,但线性探测的缺陷是产生冲突的数据堆积在一块,因为找空位置的方式就是挨着往后逐个去找,导致第二次冲突时,空位置更加难找
  2. 二次探测,为了避免线性探针的缺陷,改变找下一个空位置的方法为:H₂ = (H₁+i²) % m 或这:H₂ = (H₁+i²) % m,其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是存储空间的大小

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。因此线性探测采用标记的伪删除法来删除一个元素。

开散列法又叫Hash桶/拉链法,先对集合中的元素用Hash函数计算每个元素的存储地址,然后将相同地址的归于同一个集合,每个集合称作一个桶,桶中的元素用链表连接起来,链表的头结点存储到Hash表中

在这里插入图片描述
拉链法可以看做把大集合中的搜索问题转化成了小集合中的搜索问题,从而提高了搜索效率

这种办法的局限性在于,如果哈希冲突非常严重,则就失去了Hash表搜索的意义,此时可以将哈希冲突严重的小集合再进行转化,例如将其转化成另一个哈希桶或者一棵搜索树

实现

在这里插入代码片

性能

虽然哈希表中会有冲突的发生,其会影响到搜索的效率,但整体上哈希冲突的频率并不高,而且冲突个数时可控的,每个桶中链表的长度也是常数,因此通常意义下,哈希表的插入/删除/查找时间复杂度都为O(1)

与Java集合类之间的关系

  • hashMap和hashSet是java中,利用哈希表实现的 Map 和 Set
  • Java中采用哈希桶的策略解决哈希冲突
  • Java中哈希桶中的链表长度达到一个阈值时,链表就会转换成搜索树(红黑树)
  • Java中计算哈希值实际上时调用类的 hashCode() 方法,进行 key 的比较时使用的是equals(),因此作为自定义类作为HashMap、HashSet的 key 时,必须覆写HashCode()和equals(),而且要保证两个对象equals()相等时,HashCode()必然相等

HashMap

HashMap是Java利用哈希表实现 Map 接口的一个类,它继承自 AbstractMap类 下面将分模块化介绍Java官方的HashMap是如何实现的
在这里插入图片描述

初始默认容量

Java底层实现的HashMap初始设置的默认容量为16
在这里插入图片描述

HashMap 下的哈希表设置的最大容量为2^30

在扩容操作中,每次扩容时,都会与最大容量进行比较,若已达到最大容量,则扩容失败
在这里插入图片描述

HashMap中有一个负载因子 α

它是存储空间(散列表)装满程度的标志因子,定义式为:
α = 存入表中的元素个数 / 表的总长度,由于表的长度为定值,所有 α 就与存入表中元素的个数成成正比,即 α 越大,存入表中的元素越多,冲突发生的可能性就越大,反之越小
在这里插入图片描述
上表为负载因子 α 与冲突率的关系图,由此可以看出当冲突率过于严重达到某个阈值时,就需要通过降低负载因子的方法降低冲突率,而由于存入元素个数不可减,所以其方法就是增大散列表长度,即扩容将降低 α 从而降低冲突率

HashMap中默认的负载因子阈值为 0.75
在这里插入图片描述

哈希桶中链表转化成红黑树

在这里插入图片描述
当一个哈希桶内链表长度大于8时,该链表就会自动转化成红黑树
在这里插入图片描述
若红黑树中节点小于6时,红黑树退还为链表
在这里插入图片描述

哈希函数

当 key 为空时,返回0号桶;
当不为空时,首先通过自定义类 key 覆写的hashCode()方法,获取到相应的哈希值,在将获取到的哈希值的低16bit位与高16bit位异或,主要用于hashMap较小时所有的bit位都参与了运算
获取到哈希地址后,计算桶号的方式为:index = (table.length - 1) & hash
在这里插入图片描述
在这里插入图片描述

根据key获取value

通过key计算出其哈希地址,然后借助哈希地址在哈希桶中找到与key对应的节点
如果节点为null,返回null
如果节点不为空,返回该节点中的value
HashMap 中允许存在空节点
在这里插入图片描述

插入节点

先使用key借助hash函数计算key的哈希地址
将key-value键值对,结合计算出的hash地址插入到哈希桶中
从以下代码中可以看到,HashMap在插入时,并没有处理线程安全问题,因此HashMap不是线程安全的
在这里插入图片描述

删除key

在这里插入图片描述
通过removeNode()方法判断其返回值,不为 null 则删除成功,为 null 则删除失败
具体的removeNode()会先在散列表中找到该节点,然后分情况删除

扩容

在Java8中,HashMap 其散列表的扩容不只是简单的搬移数组,而会做移位操作。

移位操作是根据 HashMap 在计算每个插入元素的存储位置时的 & 运算所提出的,其本质就是看其哈希值的某一位是 1 还是 0,而当扩容以后,空间长度会增长一倍,即 & 运算时 比较的位数就多了一位,由多出来的这一位是 0 还是 1 来决定是否进行移位,而具体的移位距离,也是可知的,即为原数组的大小

是否移位,由扩容后表示的最高位是否1为所决定,并且移动的方向只有一个,即向高位移动。因此,可以根据对最高位进行检测的结果来决定是否移位,从而可以优化性能,不用每一个元素都进行移位,这也是其为什么要按照2倍方式扩容的第二个原因

HashMap的线程安全问题

由于 HashMap 的方法没有实现同步锁,所以在多线程环境下会出现线程安全问题,其中一个问题就出现在多线程环境下的扩容方法时,因为其扩容前后会改变结点之间的引用,而此时就会出现引用指向错误链表成环的情况

例如:在这样一种场景下
此时HashMap的容量为4,其中已放入了两个元素,5、9,分布图如下
在这里插入图片描述

扩容后:
在这里插入图片描述
其扩容后,元素的搬移过程进行了三个不可再分的步骤
而在多线程环境下,假设此时有两个线程t1,t2同时进行扩容操作
则如果出现以下情况:
t1线程在执行阶段执行了1、2两步骤,此时刻t2开始执行,因为检查到没有完成扩容,重新开始执行扩容的三个步骤
在这里插入图片描述
上图,黑色指向关系是t1线程结束后操作的结果,红色指向关系是t2线程结束后操作的结果,因此最终的指向关系会变成
在这里插入图片描述
此时就会出现链表成环,造成程序死循环

想要避免在多线程环境下使用Hashmap造成的线程问题就需要改用HashTable存储或者使用Collections.synchronizedMap,但是这两种Map的容器其处理方式都是对整个集合加锁,即多个线程同时操作时,无论是否会影响到安全问题,总是只能有一个线程在执行态,其他皆为阻塞态,这样就大大降低了效率

HashTable

在jdk中,HashTable与HashMap的底层实现基本类似,所以此处指重点介绍其与HashMap的不同之处

  • 继承的类不同:HashMap继承自AbstractMap类,AbstractMap是基于Map接口实现的,而HashTable继承自Dictionary类,而Dictionary是所有映射关系键映射值的类的父类,但两者均实现了Map接口
  • 对特殊值null的敏感程度不同:HashMap可以允许存储一个key为 null value 没有限制,HashTable则对key与value都不允许为 null
  • 线程安全不同性:HashMap没有实现同步方法,多线程环境下线程不安全,而HashTable的方法时同步的,多线程下线程安全(如果要在多线程环境下使用HashMap,可使 用Collection.synchronizedMap 间接的使用HashMap)
  • 获取哈希值的方法不同:HashMap是先使用hashCode()获取,再对值进行前后16位异或运算最终得到,而HashTable直接使用对象的hashCode()获取
  • 其底层数组初始值和扩容大小不同:HashMap初始大小为16,并按照 new = old² 扩容,HashTable初始大小为11,按照 new = 2*old+1 扩容
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值