从零开始学Java-数据结构与算法-Hash算法

前言

这几天看到ThreadLocal相关的实现,自己跑去看了下源码,结果发现个很有意思的东西:它的hash值居然是通过AtomicInteger.getAndAdd产生的,步长也很有意思,HASH_INCREMENT = 0x61c88647。于是,问了下度娘。。

Hash函数

Hash函数又称散列函数,这个东西,说起来跟信息安全竟然能扯上五毛钱关系。数字摘要,通过hash函数将不限长度明文字符“摘要”固定长度的密文,而这密文又称数字指纹。而hash碰撞的概率,就是我们信息安全的保证追求。如果黑客能够很轻易的构造出一个具有相同hash值的另外一段信息,也就是发生了hash碰撞,那么Hash函数就非常危险,这个数字指纹就不再安全。
喂,醒醒,但凡有多一粒花生米,也不至于这样。。

回来回来。这里说的Hash跟上面扯的安全相关的概念有相似之处,但是不是一回事。。这里说的,是为了hash表这种数据存储结构,如何均匀的散落在有限长度的hash表中。所以这里说的hash函数,是指将key映射到hash表的存储位置的一个公式/函数。

据大佬所说,Object函数的hashCode在底层C++代码,是直接返回对象地址的。如果hash表长度为Integer.MAX_VALUE,那直接用也没关系。但是实际上,我们不太可能使用这么大的hash表来存放数据,空间太浪费了。所以在HashMap中,有一个hash()对Object的hasnCode再次计算。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里大概解释一下,它的思路就是通过异或把高16位的信息藏在低16位中。这样对于数组长度较少的情况,也能更加均匀的进行散列。

从上面可以看出,HashMap的hash方法并不是这里要说的Hash函数,因为它还没有完成散列到hash表的位置。而这个hash方法的目的,就是为了减少后面映射到hash表引起的hash冲突/碰撞。

直接地址法

这个最简单,就是H(k)=k,或者H(k)=ak+b (线性函数)。
原理和弊端也都高度集中:就是我知道你所有元素的范围,这些元素是连续的。就比如说,如果HashMap就是一张长度为Integer.MAX_VALUE的hash表。那么直接使用Object.hashCode返回的地址就能唯一地对应到每个hash槽,还不会产生冲突。

数字分析法

通过分析预估的元素特征,选取分布更加均匀的数字来构成散列地址。例如,局域网的IP地址,同一个局域网的前面几个总是一样的,比如:192.168.0.xxx。那么就可以直接基于后面的xxx来做散列。
适用于可以预估元素特征的场景

平方取中法

将key平方后,取中间的几位数作为散列地址
原理就是平方后放大了key的差异,选取中间的数字是为了尽可能保留该key最多的特征。
适用于key中每一位都有某些数字重复出现频度很高的情况

折叠法

将key分割成若干部分,然后取他们的叠加和作为散列地址。

  1. 移位叠加:将所有部分低位进行对齐叠加(去除进位)
  2. 边界叠加:从一端沿分割界来回折叠,然后对齐相加(去除进位)
    适用于key位数特别长的。

随机数法

这个比较容易理解,就不多说了。
适用于key长度不等的场景。

随机乘数法

选取一个随机的实数f, 0<f<1.那么任意一个key与之相乘则必然会有小数部分。再将小数部分与hash表长度相乘,其结果的整数部分就是散列地址。
好处/优点就是散列的分布均匀程度与数组长度无关。
而实数的选取,有大佬做过调查,取黄金比例数最优,即:0.6180339…

基数转换法

将key当做是其他进制看待,然后再处理成十进制,取中间的几位数。
但一般不会直接这样使用,而是结合上面的方法,整合。例如,先变基,然后再用折叠法/平方取中/随机乘数

除留余数法

这个也就是我们最熟悉的求余啦。H(k) = k % p. 这里p是小于等于表长度的最大素数。
理论研究表明,模p选取不大于表长且最接近表长的素数时效果最好。且p最好取1.1n~1.7n之间的一个素数(n为存在的数据元素个数)
只不过HashMap的实现是直接对表长度求余了。但是实际上,这个选取之后,就不能够变化了,因为如果一旦变化了,就意味着必须重新进行hash。而HashMap是不限制表长度的(rehash),而且一般都是一个一个往里面加元素的。所以他是假定我每次添加的元素n都是3/4表长度m。反过来算一下,p=m=4/3,也能符合上面理论的规律。

字符串数值hash法

用一个很高大上的ELFHash(Executable and Linking Format 可执行链接格式)函数。具体咋样咱也不懂。。
不过,String的hash不是这么实现的。而是s[0]*31^(n-1) + s[1]31^(n-2) + … + s[n-1]。原因嘛,简单高效。31是个奇质数(奇素数)而选取31,还有另外一个原因,是不能使相乘之后溢出太多,造成key的特点丢失太多。所以31i=32i-i=(i<<5)-i。素数提高了分布的均匀性,而位运算与减法运算提高了效率。如果是其他数字不能用位运算或加减运算来替代乘除运算的,那效率肯定要低一些。还有另外一个原因,就是相乘之后不能溢出或溢出太多,使得key的特点丢失,造成hash碰撞加剧的情况。

小结

总的来说,使用什么散列函数,具体还是得看实际需要,最大程度上保证散列均匀的同时,考虑效率和空间利用。主要考虑点:

  • 计算hash所花费的时间
  • key的长度
  • hash表长度
  • key的分布情况
  • 查找频率-查找频率高的,更要考虑hash的效率。

而HashMap采用的除留余数法,还为了更好的分布,将高位信息藏到了低位中,让数组长度较小的情况下最大程度的做到不丢失高位的信息,从而让hash分布更加均匀。

上述方法的根本目的在于使key在hash表中尽量分布均匀。但是实际上的分布还是得根据实际的key进行分布。怎么说呢,如果key的分布本来就比较集中,那发生碰撞的概率也不会太低。就好比一个人,用再好的化妆技术,总无法改变其本来面目,除非整容。

后记

太晚了,光是看大佬的博客和理解就花了不少时间。后面还有hash碰撞的处理方法,然后才能再说回咱们的ThreadLocal中的ThreadLocalMap中的实现——它跟HashMap的实现不一样。先睡了。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值