哈希表及底层的hash函数

哈希表

概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键
码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中
元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素如果构造一种存储结构,通过某种函
(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快
找到该元素
image-20220228152800547

image-20220228152827604


冲突-概念

对于两个数据元素的关键字 和 (i != j),有 != ,但有:Hash( ) == Hash( ),即:不同关键字通过相同哈
希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

把具有不同关键码而具有相同哈希地址的数据元素称为

冲突-避免

首先,我们需要明确一点,由于我们哈希表底层数组的容量往往是小于实际要存储的关键字的数量的,这就导致一
个问题,冲突的发生是必然的,但我们能做的应该是尽量的降低冲突率

避免冲突:哈希函数的一个设计(冲突在某种程度上来说是不可能完全避免)

引起哈希冲突的一个原因可能是:哈希函数设计不够合理哈希函数设计原则

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1
    之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见的哈希函数

1.直接定制法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关
键字的分布情况 使用场景:适合查找比较小且连续的情况 面试题

2.除留余数法

设散列表中允许的地址数为m**,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:**Hash(key) = key% p(p<=m),将关键码转换成哈希地址(实际上不用去关心是否是质数,影响微乎其微)

image-20220228154746884

冲突-避免-负载因子调节(重点掌握 )

负载因子 = 存储散列表的元素个数/散列表的长度

image-20220228160003138

所以当冲突率达到一个无法忍受的程度时,我们需要通过降低负载因子来变相的降低冲突率。
已知哈希表中已有的关键字个数是不可变的,那我们能调整的就只有哈希表中的数组的大小 (扩容)


解决冲突

解决哈希冲突两种常见的方法是:闭散列和开散列

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

  1. 线性探测
    比如上面的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,下标为4,因此44理论上应该插在该
    位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
    线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
    • 插入
      通过哈希函数获取待插入元素在哈希表中的位置
      如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到
      下一个空位置,插入新元素
image-20220228161149151

上面一段说了关于闭散列的一些操作和问题,比如我们遇到哈希冲突的时候,可以去找到该冲突下一个不为空的位置去插入,那么在删除的时候也会受到影响,当你把4元素删除的时候,你再去查找44,就没有了指向标(此时你可以把4位置作为标记位,删除和插入的时候能让计算机准确的去知道该元素是否被插入过),这个问题解决了之后,还有一个问题就是闭散列会把冲突元素按照插入顺序来进行摆放,此时引出二次探测概念。


二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨

着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:Hi = (H0+i2)%m 其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,

m是表的大小。 对于2.1中如果要插入44,产生冲突,使用解决后的情况为

image-20220228161917121

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不
会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情
况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
**因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷 **


冲突-解决-开散列/哈希桶(重点掌握)

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子
集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

image-20220228163339195

在底层链表采用的是尾插(等下我们实现用头插实现),由于负载因子的限制,链表的长度不会很长,所以在链表里去遍历的时候时间复杂度不会到达O(N)

image-20220228163708325


性能分析

虽然哈希表一直在和冲突做斗争,但在实际使用过程中,我们认为哈希表的冲突率是不高的,冲突个数是可控的,
也就是每个桶中的链表的长度是一个常数,所以,通常意义下,我们认为哈希表的插入/删除/查找时间复杂度是
O(1)

面试题:HashMap的处理方式和ConcurrentHashMap线程安全的区分


image-20220228234953000

HashMpa的源码分析

关于解决上图的前三个问题实际上是要我们进入源码才能找到答案。

image-20220301113527143 image-20220301113550705 image-20220301113603016 image-20220301113707485

根据源码看见,HashMap有三个构造函数,无论哪个都是没有开始new数组的,此时应该去put函数里找

image-20220301113903082

在put函数里,假如第一次放入元素,此时table数组为空,因为你还没new好数组,也没放入元素,此时应该执行第一个if,然后通过resize函数来进行扩容,在赋给tab,将长度又赋给n。


resize函数

进入这个函数,最开始的时候:oldTab是空,进入第一个else(原因是:如果你当时构造的是无参函数,那么你只规定了负载因子,如果你的构造函数是2参的时候,你可以去找到最后面的threahold被tableSzieFor()函数赋值了,此时进入resize函数中oldThr = threshold,那么你就不会进入第一个else,而是进入第二个else if),假设调用的是无参构造,此时newCap 和 newThr都被赋值了,也就是说数组容量确定了,resize函数结束后,就开始new数组了,以上都是表明数组在第一次put的时候才开辟内存。

image-20220301114140050

实际上,putVal是被put函数调用的,因为putVal函数其实是来算key的hashCode,将计算方式封住成一个函数,有些人会有疑问:

key的hashCode不就是通过key的hash函数生成的hashCode值去%容量吗?为啥还要封住成函数呢?

实际上:HashMap底层并不是完全这样去执行的。

在这里插入图片描述

按理说应该是公式:key.hashCode % capacity = index,但是底层是通过hashCode与hashCode右移16位来生成的,其实计算结果都是一样,但是方式不一样,接下来仔细说一下:

浅谈HashMap中的hash算法

image-20220301115643339

(n-1)& hash 保证n是偶数,此时n-1就是奇数,那么而hash的可能是奇数也可能是偶数,此时就为了hash的高位和低位,增加了低位的随机性,而且混合后的值也变相的保留了高位的随机性

image-20220301115244042

这里还需要补充一点:当构造函数是有参构造时候(假如是两个参数),你放进去的初始容量会被tableSizeFor函数接收,返回一个

cap >= capacity 且cap满足是2的次幂,比如放进去20,返回的应该是32,此时n就是31,因为n就是数组长度啊。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFjeeh9O-1646110653656)(E:/MarkDown/note.1/image/image-20220301122639093.png)]

走到put的这里,开始在数组下标放元素,jdk1.8后是尾插,此时查完之后你需要去判断树化的条件,如果链表的长度大于8,我们进去treeifyBin()函数,然后进入函数再去判断数组长度是否>64,都满足之后才开始树化,转变成红黑树

image-20220301123438811

resize函数是扩容函数(超过负载因子时候),在put函数里最后去判断如果满足,进去扩容,此时第一个else if 的执行语句是让newCap = oldCap*2,之所以扩容两倍是保证容量是2的倍数,这样n是偶数,n-1是奇数,满足hash函数的设计。

image-20220301124616215
653657)]

resize函数是扩容函数(超过负载因子时候),在put函数里最后去判断如果满足,进去扩容,此时第一个else if 的执行语句是让newCap = oldCap*2,之所以扩容两倍是保证容量是2的倍数,这样n是偶数,n-1是奇数,满足hash函数的设计。

[外链图片转存中…(img-EvYEoh90-1646110653657)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值