携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
1.4.17 hashtable 扩容和如何解决冲突
参考回答
-
哈希表的扩容
(1)为什么要扩容
使用链地址法封装哈希表时, 填装因子(loaderFactor)会大于1,理论上这种封装的哈希表时可以无限插入数据的但是但是随着数据量的增多,哈希表中的每个元素会变得越来越长, 这是效率会大大降低。 因此,需要通过扩容来提高效率。
(2)如何扩容
Hashtable每次扩容,容量都为原来的2倍加1,而HashMap为原来的2倍。此时,需要将所有数据项都进行修改(需要重新调用哈希函数,来获取新的位置)。 哈希表扩容是一个比较耗时的过程,但是一劳永逸。
(3)什么情况下扩容
常见的情况是在填装因子(loaderFactor) > 0.75是进行扩容。
(4)扩容的代码实现(详见解析答案解析代码)
-
如何解决哈希冲突
解决哈希冲突通常有开放地址法和链地址法两种方法,分别如下:
开放定址法
即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。比较常用的探测方法有线性探测法,比如有一组关键字
{12,13,25,23,38,34,6,84,91},Hash表长为14,Hash函数为address(key)=key%11,当插入12,13,25时可以直接插入,而当插入23时,地址1被占用了,因此沿着地址1依次往下探测(探测步长可以根据情况而定),直到探测到地址4,发现为空,则将23插入其中。链地址法
采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址。
答案解析
-
哈希表的使用
Hash表采用一个映射函数
f :key -> address将关键字映射到该记录在表中的存储位置,从而在想要查找该记录时,可以直接根据关键字和映射关系计算出该记录在表中的存储位置,通常情况下,这种映射关系称作为Hash函数,而通过Hash函数和关键字计算出来的存储位置(注意这里的存储位置只是表中的存储位置,并不是实际的物理地址)称作为Hash地址。 -
哈希函数的设计
Hash函数设计的好坏直接影响到对Hash表的操作效率。通常有以下几种构造Hash函数的方法:
直接定址法
取关键字或者关键字的某个线性函数作为Hash地址,即
address(key) = a*key + b。平方取中法
对关键字进行平方计算,然后取结果的中间几位作为Hash地址,假如有以下关键字序列
{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取中间的两位数{72,89,00}作为Hash地址。折叠法
将关键字拆分成几个部分,然后将这几个部分组合在一起,以特定的方式进行转化形成Hash地址。例如假如知道某图书的SBN号为:
8903-241-23,可以将address(key)=89+03+24+12+3作为Hash地址。除留取余法
如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,
address(key)=key % p。在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。
-
哈希表大小的确定
Hash表大小的确定非常关键,如果Hash表的空间远远大于最后实际存储的记录个数,就会造成较大的空间浪费。如果选取小了的话,则容易造成冲突。在实际情况中,一般需要根据最终记录存储个数和关键字的分布特点来确定Hash表的大小。还有一种情况时可能事先不知道最终需要存储的记录个数,则需要动态维护Hash表的容量,此时可能需要重新计算Hash地址。