面经手册 · 第 2 篇《数据结构,HashCode 为什么使用 31 作为乘数?》
作者:小傅哥
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、前言
在面经手册的前两篇介绍了[《面试官都问我啥》]()和[《认知自己的技术栈盲区》](),这两篇内容主要为了说明面试过程的考查范围,包括个人的自我介绍、技术栈积累、项目经验等,以及在技术栈盲区篇章中介绍了一个整套技术栈在系统架构用的应用,以此全方面的扫描自己有哪些盲区还需要补充。而接下来的章节会以各个系列的技术栈中遇到的面试题作为切入点,讲解技术要点,了解技术原理,包括;数据结构、数据算法、技术栈、框架等进行逐步展开学习。
在进入数据结构章节讲解之前可以先了解下,数据结构都有哪些,基本可以包括;数组(Array)
、栈(Stack)
、队列(Queue)
、链表(LinkList)
、树(Tree)
、散列表(Hash)
、堆(Heap)
、图(Graph)
。
而本文主要讲解的就是与散列表相关的HashCode
,本来想先讲HashMap
,但随着整理资料发现与HashMap
的实现中,HashCode
的散列占了很重要的一设计思路,所以最好把这部分知识补全,再往下讲解。
二、面试题
说到HashCode的面试题,可能这是一个非常核心的了。其他考点;怎么实现散列、计算逻辑等,都可以通过这道题的学习了解相关知识。
Why does Java's hashCode() in String use 31 as a multiplier?
这个问题其实☞指的就是,hashCode的计算逻辑中,为什么是31作为乘数。
三、资源下载
本文讲解的过程中涉及部分源码等资源,可以通过关注公众号:bugstack虫洞栈,回复下载进行获取{回复下载后打开获得的链接,找到编号ID:19},包括;
HashCode 源码测试验证工程,
interview-03
103976个英语单词库.txt,验证HashCode值
HashCode散列分布.xlsx,散列和碰撞图表
四、源码讲解
1. 固定乘积31在这用到了
在获取hashCode
的源码中可以看到,有一个固定值31
,在for循环每次执行时进行乘积计算,循环后的公式如下;
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
那么这里为什么选择31作为乘积值呢?
2. 来自stackoverflow的回答
在stackoverflow
关于为什么选择31作为固定乘积值,有一篇讨论文章,Why does Java's hashCode() in String use 31 as a multiplier? 这是一个时间比较久的问题了,摘取两个回答点赞最多的;
413个赞👍的回答
最多的这个回答是来自《Effective Java》的内容;
这段内容主要阐述的观点包括;
31 是一个奇质数,如果选择偶数会导致乘积运算时数据溢出。
另外在二进制中,2个5次方是32,那么也就是
31 * i == (i << 5) - i
。这主要是说乘积运算可以使用位移提升性能,同时目前的JVM虚拟机也会自动支持此类的优化。
80个赞👍的回答
这个回答就很有实战意义了,告诉你用超过5千个单词计算hashCode,这个hashCode的运算使用31、33、37、39和41作为乘积,得到的碰撞结果,31被使用就很正常了。
他这句话就就可以作为我们实践的指向了。
3. Hash值碰撞概率统计
接下来要做的事情并不难,只是根据stackoverflow
的回答,统计出不同的乘积数对10万个单词的hash计算结果。10个单词表已提供,可以通过关注公众号:bugstack虫洞栈进行下载
3.1 读取单词字典表
单词表的文件格式如上,可以自行解析
读取文件的代码比较简单,这里不展示了,可以通过
资源下载
进行获取
3.2 Hash计算函数
这个过程比较简单,与原hash函数对比只是替换了可变参数,用于我们统计不同乘积数的计算结果。
3.3 Hash碰撞概率计算
想计算碰撞很简单,也就是计算那些出现相同哈希值的数量,计算出碰撞总量即可。这里的实现方式有很多,可以使用set
、map
也可以使用java8
的stream
流统计distinct
。
这里记录了最大hash和最小hash值,以及最终返回碰撞数量的统计结果。
3.4 单元测试
以上先设定读取英文单词表中的10个单词,之后做hash计算。
在hash计算中把单词表传递进去,同时还有乘积数;
2, 3, 5, 7, 17, 31, 32, 33, 39, 41, 199
,最终返回一个list结果并输出。这里主要验证同一批单词,对于不同乘积数会有怎么样的hash碰撞结果。
测试结果
以上就是不同的乘数下的hash碰撞结果图标展示,从这里可以看出如下信息;
乘数是2时,hash的取值范围比较小,基本是堆积到一个范围内了,后面内容会看到这块的展示。
乘数是3、5、7、17等,都有较大的碰撞概率
乘数是31的时候,碰撞的概率已经很小了,基本稳定。
顺着往下看,你会发现199的碰撞概率更小,这就相当于一排奇数的茅坑量多,自然会减少碰撞。但这个范围值已经远超过int的取值范围了,如果用此数作为乘数,又返回int值,就会丢失数据信息。
4. Hash值散列分布
除了以上看到哈希值在不同乘数的一个碰撞概率后,关于散列表也就是hash,还有一个非常重要的点,那就是要尽可能的让数据散列分布。只有这样才能减少hash碰撞次数,也就是后面章节要讲到的hashMap源码。
那么怎么看散列分布呢?如果我们能把10万个hash值铺到图表上,形成的一张图,就可以看出整个散列分布。但是这样的图会比较大,当我们缩小看后,就成一个了大黑点。所以这里我们采取分段统计,把2 ^ 32方分64个格子进行存放,每个格子都会有对应的数量的hash值,最终把这些数据展示在图表上。
4.1 哈希值分段存放
这个过程主要统计
int
取值范围内,每个哈希值存放到不同格子里的数量。这里也是使用了java8的新特性语法,统计起来还是比较方便的。
4.2 单元测试
这里列出我们要统计的乘数值,每一个乘数下都会有对应的哈希值数量汇总,也就是64个格子里的数量。
最终把这些统计值放入到excel中进行图表化展示。
统计图表
以上是一个堆积百分比统计图,可以看到下方是不同乘数下的,每个格子里的数据统计。
除了199不能用以外,31的散列结果相对来说比较均匀。
4.2.1 乘数2散列
乘数是2的时候,散列的结果基本都堆积在中间,没有很好的散列。
4.2.2 乘数31散列
乘数是31的时候,散列的效果就非常明显了,基本在每个范围都有数据存放。
4.2.3 乘数199散列
乘数是199是不能用的散列结果,但是它的数据是更加分散的,从图上能看到有两个小山包。但因为数据区间问题会有数据丢失问题,所以不能选择。
文中引用
http://www.tianxiaobo.com/2018/01/18/String-hashCode-%E6%96%B9%E6%B3%95%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9%E6%95%B0%E5%AD%9731%E4%BD%9C%E4%B8%BA%E4%B9%98%E5%AD%90/
https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier
五、总结
以上主要介绍了hashCode选择31作为乘数的主要原因和实验数据验证,算是一个散列的数据结构的案例讲解,在后续的类似技术中,就可以解释其他的源码设计思路了。
看过本文至少应该让你可以从根本上解释了hashCode的设计,关于他的所有问题也就不需要死记硬背了,学习编程内容除了最开始的模仿到深入以后就需要不断的研究数学逻辑和数据结构。
文中参考了优秀的hashCode资料和stackoverflow,并亲自做实验验证结果,大家也可以下载本文中资源内容;英文字典、源码、excel图表等内容。
六、推荐阅读
版权声明: 本文为 InfoQ 作者【小傅哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/c7a60d69589df3bd60d59d94a】。文章转载请联系作者。
评论 (2 条评论)