趣谈 Hash。

    面试官: 聊聊HashMap的底层实现。

答: 这就得从hash算法说起了。

查找速度的困扰


算法国自建立起,就以快速为至高的荣誉,O(n^2) 时间复杂度的设计常常被人嫌弃,一般都想着弄个O(logn)


算法国最近遇到了一个问题,就是随着处理数据的逐步增大,查找的时间越来越大了


之前用的数组和链表,最后改成二叉查找树,可是这些都需要和其中的元素进行比较,比较的次数越多,查询的速度就越慢


国王想着能不能有一种办法,查找某个元素的时候,不需要比较,直接就得到这个元素,时间复杂度直接就为O(1),和集合中的元素个数就没有关系了


初次的方案


国王召集各大臣讨论此事


“如果不需要比较,我们可以借鉴数组下标访问的思路来做”,经验老道的王大臣发话了


“哦,怎么个设计?”何大臣问道


“你看,我们要访问数组中某个元素怎么访问?”


“只需要知道起始位置和下标值就可以了,不管数组中有多少个元素,都可以一次访问到,这正是因为起始位置和下标值组成了元素的存储位置,从这个存储位置就可以直接找到元素”,王大臣自问自答起来


左丞相接着道,“所以说,给定一个元素,我们只要得到该元素的位置,也就是说将元素和元素位置建立一种一一对应的关系,就可以迅速定位要查的元素了”,说着说着,王大臣在纸上画起了图


Hash函数的出现


“这也太理想了吧,现实生活中要存储的元素(key)的取值范围一般很大,比如说正整数,不可能为无穷多个正整数分配无穷大的数组吧,这不现实



就算有无穷大的数组,但实际存储的元素的范围可能很小,比如输入大多数在1-100之间,只有极少数的是其他数,那预先分配的那些空间不是浪费了吗?”,何大臣立马驳回了王大臣的想法


“那依你看,如何是好?”王大臣问道


“我看,既然输入的元素的范围可能很大甚至无穷,而我们的内存有限,所以说我们需要一种函数映射关系,将这些无限的元素映射到我们有限的内存地址上”,说着说着画了一个图



“很显然,多个数据有可能被映射到同一个地址上,我们暂且称为冲突吧”,何大臣说道


“我们给这个函数映射起一个名字,就叫Hash函数吧,这个Hash函数代表着一类函数,即把任意范围的元素可以通过映射关系压缩成固定范围的元素”何大臣说道,大家一致同意


Hash函数的选择


“那现在的问题就很清晰了,一个是这个Hash函数该选择什么样具体的函数,另一个就是出现冲突该怎么办?”王大臣说道


“如果是正整数,我们可以用这个正整数数除以某个数,取其余数,即我们常用的 k % m,k为正整数,m 为除数


这样一来,范围就缩小了很多,比如说 15%10=5,26%10=6,...,所有的正整数经过运算,都变成了 0-9 范围之间的数了,这样范围就缩小了很多”何大臣发挥了他数学的才能


大家一致称赞


m 的选择


“如果是这种做法,m 的选择就非常重要了,如果 k 值分布均匀还无所谓,如果 k 值具有某些特征


比如说 k 的个位基本上不变,而高位分布均匀,如 15,25,45,65,85,95,155,这么一组数据,经过你刚才的 k%10不就全部落在5这个位置上了吗?”一直没有说话的另一个李大臣发话了


 这个李大臣虽少言寡语,但非常有才能


“对对对”,何大臣连声说道


“我们必须要使得经过Hash函数后关键字的分布均匀,尽量减少冲突,所以针对不同类型的关键字要有自己特定的Hash函数,整数应该有整数的Hash函数,字符串应该有字符串的Hash函数


就算针对同一类型的关键字,如果它具有某种规律,我们也要具体情况具体设计,刚才李大臣说的那种情况,我们就得选取其他分布均匀的高位来进行Hash”右丞相补充道


“那为何不优化一下我们的Hash函数来使得关键字分布均匀,比如说 m 取一个素数,这样就不会出现刚才的情况了,比如说m取11,15,25,45,65,85,95,155,这些数Hash后就会变成这个”,李大臣说着说着画了一个表



“这样就分布均匀,冲突很少,所以我们只需要用一个长度为11的数组来存储就行,只比你原先的10多了一个长度”李大臣补充道


“如果事先知道元素的分布情况,那我们可以针对特定的元素来设计Hash函数,如果不知道的话,那我们就提前想一个比较好的Hash函数,来应对各种可能出现的情况


不管怎样,我们最终的目的就是让各个关键字均匀的Hash到不同的位置”。


“Hash函数的选择我们已经有了具体的方案了,那就只剩下冲突的解决问题了”,王大臣发话。

“要解决冲突其实也不难,既然会有多个元素被Hash到同一个位置,而这个位置只能存储一个元素,那么我让这个位置可以存储多个元素不就可以了吗?”,何大臣说道


“哦,怎么个弄法?”王大臣问道


“用链表啊,来一个元素加一个,让这个位置存储一个指针,指向一个链表,让所有相同位置的元素都放在这个链表中”,何大臣回答道,接着又画了一个图

 

“在存储的时候,如果多个元素被Hash到同一位置,那么就加入到该位置所指向的链表中,如果该位置没有元素,则为null(指向空)”,何大臣解释道


“那个 1 和 6 谁先存放进来呢?”,思维缜密的王大人问道


“这个当然是6了,因为这个插入链表的时候要采用‘头插’的方式,也就是插入链表的最前面(图中里数组最近的元素)”,何大人说道


"哦,这是为什么呢?",王大人追问道


“因为经常发生这样的事情:新加入的元素很可能被再次访问到,所以放到头的话,如果查找就不用再遍历链表了”,见多识广的何大人解释道


rehash


“这样解决冲突固然好,但是也有瓶颈啊”,寡言的李大臣又发言了


“哦,什么瓶颈?”,何大人问道


“你看啊,假设咱们的Hash函数设计的非常好,能够将元素均匀Hash(散列)开来,但是当我们实际存入的值越来越多的时候,这个链表也势必越来越长,那当我们进行查找的时候,势必就会遍历链表,效率也就越来越慢”,李大臣回应道,顺便画了一个图




“这样的话,随着链表的不断增长,查询某一个元素的时间也就增加了,如果链表长度远远大于数组长度,不就和用链表存储一样了吗?”,李大臣说道


“恩恩,对,李大臣说的极是,李大臣有何高见?”,何大臣问道


“现在只能扩大数组的长度大约为原来的两倍


然后选取一个相关的新的Hash函数(比如之前使用 key % m,现在只改变一下m的值)


将旧Hash表中所有的元素通过新的Hash函数计算出新的Hash值,并将其插入到新表中(仍然使用链表),这就叫rehash吧”,李大臣说完又画了一个图



这里的数组就扩大了近两倍,由于要大小要选素数,那就选原数组大小两倍后的第一个素数7,旧Hash表和新Hash表采用了不同的Hash函数,但相关,只是m的取值变了”李大臣解释道


“哦,这样做确实是一种办法,但是问题随之而来,就是什么时候开始rehash”,何大臣说道


装载因子 α 


“我们可以定义这样一个变量 α = 所有元素个数/数组的大小,我们叫它装载因子吧,它代表着我们的Hash表(也就是数组)的装满程度,在这里也代表链表的平均长度


比如说,我们的数组大小为 5 ,我们给里面存入 3个元素,那么  α = 3/5 =0.6, 这个Hash表装满程度为60%,平均每条链有0.6个元素,当然 α 也可以等于和大于 1 ”,李大臣说道


“哦,引入这个装载因子有何用意?”,何大臣问道


这个装载因子代表了Hash表的装满程度,这里也可以代表链表的平均长度,那么也就可以代表查询时的时间长短了


基于此,我们为了不让查询时间长,也就是查询性能低,我们可以设置一个临界 α 值,当随着存入元素导致 α 大于这个临界 α 值的时候


我们可以通过rehash来调整当前的 α 值,使之低于我们设定的 临界 α 值,从而使我们的查询性能保持在较好的范围之内”,李大臣答道


“比如说,我们设定 临界 α = 0.7,对于一个Hash表大小为5的Hash表而言


当存入存入第四个元素的时候,α 就超出了临界 α 值,我们可以将数组长度变为11进行rehash(因为11是原表两倍后的第一个素数),使得装载因子 α 小于 0.7”,李大臣举了一个例子并画了一个图


“通过rehash我们可以使得装载因子在一定范围内,那我们的查询性能也就得到了保证了”,李大臣说道


“哦,那这个 临界的 α 值应该选择多大呢?”何大臣追问道


“这个 临界 α 如果选的小了,那数组的空间利用率就会太低,就比如说数组大小为100,α = 0.01,那装满程度为1%,99%还没有被利用


如果 α 太大了,那冲突就会很多,比如说 数组大小为 5,α = 10, 那平均每条链有10个元素,装满程度为1000%


即使Hash函数设计的合理,基本上每次存放元素的时候就会冲突,所以鉴于两者之间我觉得 0.6 - 0.9 之间是一个不错的选择,不妨选0.75吧”,李大臣回答道


大家一致同意


最后的总结


“这两天辛苦众爱卿了,你们的方案非常好,我也听了听


我们这种用Hash函数将关键字和关键字地址建立起来的映射,对我们的查找非常有帮助


其中非常重要的关键点就是:哈希函数的选择、处理冲突的方法以及装载因子调整,接下来我们把这个点子应用到我国的查找行业,我相信一定会有很大的提高”,国王做了最后的总结。







                                                                                                           

                                                                                                                         转发:  微信公众号:趣谈编程




  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值