String hashCode 方法为什么选择数字31作为乘子Word格式.docx

上传人:b****5 文档编号:18614073 上传时间:2022-12-29 格式:DOCX 页数:10 大小:3.21MB
下载 相关 举报
String hashCode 方法为什么选择数字31作为乘子Word格式.docx_第1页
第1页 / 共10页
String hashCode 方法为什么选择数字31作为乘子Word格式.docx_第2页
第2页 / 共10页
String hashCode 方法为什么选择数字31作为乘子Word格式.docx_第3页
第3页 / 共10页
String hashCode 方法为什么选择数字31作为乘子Word格式.docx_第4页
第4页 / 共10页
String hashCode 方法为什么选择数字31作为乘子Word格式.docx_第5页
第5页 / 共10页
点击查看更多>>
下载资源
资源描述

String hashCode 方法为什么选择数字31作为乘子Word格式.docx

《String hashCode 方法为什么选择数字31作为乘子Word格式.docx》由会员分享,可在线阅读,更多相关《String hashCode 方法为什么选择数字31作为乘子Word格式.docx(10页珍藏版)》请在冰豆网上搜索。

String hashCode 方法为什么选择数字31作为乘子Word格式.docx

这里我来简单推导一下这个公式:

假设n=3

i=0->

h=31*0+val[0]

i=1->

h=31*(31*0+val[0])+val[1]

i=2->

h=31*(31*(31*0+val[0])+val[1])+val[2]

h=31*31*31*0+31*31*val[0]+31*val[1]+val[2]

h=31^(n-1)*val[0]+31^(n-2)*val[1]+val[2]

上面的公式包括公式的推导并不是本文的重点,大家了解了解即可。

接下来来说说本文的重点,即选择31的理由。

从网上的资料来看,一般有如下两个原因:

第一,31是一个不大不小的质数,是作为hashCode乘子的优选质数之一。

另外一些相近的质数,比如37、41、43等等,也都是不错的选择。

那么为啥偏偏选中了31呢?

请看第二个原因。

第二、31可以被JVM优化,31*i=(i<

<

5)-i。

上面两个原因中,第一个需要解释一下,第二个比较简单,就不说了。

下面我来解释第一个理由。

一般在设计哈希算法时,会选择一个特殊的质数。

至于为啥选择质数,我想应该是可以降低哈希算法的冲突率。

至于原因,这个就要问数学家了,我几乎可以忽略的数学水平解释不了这个原因。

上面说到,31是一个不大不小的质数,是优选乘子。

那为啥同是质数的2和101(或者更大的质数)就不是优选乘子呢,分析如下。

这里先分析质数2。

首先,假设 

n=6,然后把质数2和n带入上面的计算公式。

并仅计算公式中次数最高的那一项,结果是2^5=32,是不是很小。

所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。

也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升。

上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?

根据上面的分析,我想大家应该可以猜出结果了。

就是不用再担心哈希值会分布在一个小的区间内了,因为101^5=10,510,100,501。

但是要注意的是,这个计算结果太大了。

如果用int类型表示哈希值,结果会溢出,最终导致数值信息丢失。

尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。

最后,我们再来看看质数31的计算结果:

31^5=28629151,结果值相对于32和10,510,100,501来说。

是不是很nice,不大不小。

上面用了比较简陋的数学手段证明了数字31是一个不大不小的质数,是作为hashCode乘子的优选质数之一。

接下来我会用详细的实验来验证上面的结论,不过在验证前,我们先看看StackOverflow上关于这个问题的讨论,WhydoesJava'

shashCode()inStringuse31asamultiplier?

其中排名第一的答案引用了《EffectiveJava》中的一段话,这里也引用一下:

Thevalue31waschosenbecauseitisanoddprime.Ifitwereevenandthemultiplicationoverflowed,informationwouldbelost,asmultiplicationby2isequivalenttoshifting.Theadvantageofusingaprimeislessclear,butitistraditional.Anicepropertyof31isthatthemultiplicationcanbereplacedbyashiftandasubtractionforbetterperformance:

`31*i==(i<

5)-i``.ModernVMsdothissortofoptimizationautomatically.

简单翻译一下:

选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。

选择质数的优势并不是特别的明显,但这是一个传统。

同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:

31*i==(i<

5)-i,现代的Java虚拟机可以自动的完成这个优化。

排名第二的答案设这样说的:

AsGoodrichandTamassiapointout,Ifyoutakeover50,000Englishwords(formedastheunionofthewordlistsprovidedintwovariantsofUnix),usingtheconstants31,33,37,39,and41willproducelessthan7collisionsineachcase.Knowingthis,itshouldcomeasnosurprisethatmanyJavaimplementationschooseoneoftheseconstants.

这段话也翻译一下:

正如Goodrich和Tamassia指出的那样,如果你对超过50,000个英文单词(由两个不同版本的Unix字典合并而成)进行hashcode运算,并使用常数31,33,37,39和41作为乘子,每个常数算出的哈希值冲突数都小于7个,所以在上面几个常数中,常数31被Java实现所选用也就不足为奇了。

上面的两个答案完美的解释了Java源码中选用数字31的原因。

接下来,我将针对第二个答案就行验证,请大家继续往下看。

3.实验及数据可视化

本节,我将使用不同的数字作为乘子,对超过23万个英文单词进行哈希运算,并计算哈希算法的冲突率。

同时,我也将针对不同乘子算出的哈希值分布情况进行可视化处理,让大家可以直观的看到数据分布情况。

本次实验所使用的数据是Unix/Linux平台中的英文字典文件,文件路径为 

/usr/share/dict/words。

3.1哈希值冲突率计算

计算哈希算法冲突率并不难,比如可以一次性将所有单词的hashcode算出,并放入Set中去除重复值。

之后拿单词数减去set.size()即可得出冲突数,有了冲突数,冲突率就可以算出来了。

当然,如果使用JDK8提供的流式计算API,则可更方便算出,代码片段如下:

publicstaticIntegerhashCode(Stringstr,Integermultiplier){

inthash=0;

str.length();

hash=multiplier*hash+str.charAt(i);

returnhash;

/**

*计算hashcode冲突率,顺便分析一下hashcode最大值和最小值,并输出

*@parammultiplier

*@paramhashs

*/

publicstaticvoidcalculateConflictRate(Integermultiplier,List<

Integer>

hashs){

Comparator<

cp=(x,y)->

x>

y?

1:

(x<

-1:

0);

intmaxHash=hashs.stream().max(cp).get();

intminHash=hashs.stream().min(cp).get();

//计算冲突数及冲突率

intuniqueHashNum=(int)hashs.stream().distinct().count();

intconflictNum=hashs.size()-uniqueHashNum;

doubleconflictRate=(conflictNum*1.0)/hashs.size();

System.out.println(String.format("

multiplier=%4d,minHash=%11d,maxHash=%10d,conflictNum=%6d,conflictRate=%.4f%%"

multiplier,minHash,maxHash,conflictNum,conflictRate*100));

结果如下:

从上图可以看出,使用较小的质数做为乘子时,冲突率会很高。

尤其是质数2,冲突率达到了55.14%。

同时我们注意观察质数2作为乘子时,哈希值的分布情况。

可以看得出来,哈希值分布并不是很广,仅仅分布在了整个哈希空间的正半轴部分,即0~231-1。

而负半轴-231 

~-1,则无分布。

这也证明了我们上面断言,即质数2作为乘子时,对于短字符串,生成的哈希值分布性不佳。

然后再来看看我们之前所说的31、37、41这三个不大不小的质数,表现都不错,冲突数都低于7个。

而质数101和199表现的也很不错,冲突率很低,这也说明哈希值溢出并不一定会导致冲突率上升。

但是这两个家伙一言不合就溢出,我们认为他们不是哈希算法的优选乘子。

最后我们再来看看32和36这两个偶数的表现,结果并不好,尤其是32,冲突率超过了了50%。

尽管36表现的要好一点,不过和31,37相比,冲突率还是比较高的。

当然并非所有的偶数作为乘子时,冲突率都会比较高,大家有兴趣可以自己验证。

3.2哈希值分布可视化

上一节分析了不同数字作为乘子时的冲突率情况,这一节来分析一下不同数字作为乘子时,哈希值的分布情况。

在详细分析之前,我先说说哈希值可视化的过程。

我原本是打算将所有的哈希值用一维散点图进行可视化,但是后来找了一圈,也没找到合适的画图工具。

加之后来想了想,一维散点图可能不合适做哈希值可视化,因为这里有超过23万个哈希值。

也就意味着会在图上显示超过23万个散点,如果不出意外的话,这23万个散点会聚集的很密,有可能会变成一个大黑块,就失去了可视化的意义了。

所以这里选择了另一种可视化效果更好的图表,也就是excel中的平滑曲线的二维散点图(下面简称散点曲线图)。

当然这里同样没有把23万散点都显示在图表上,太多了。

所以在实际绘图过程中,我将哈希空间等分成了64个子区间,并统计每个区间内的哈希值数量。

最后将分区编号做为X轴,哈希值数量为Y轴,就绘制出了我想要的二维散点曲线图了。

这里举个例子说明一下吧,以第0分区为例。

第0分区数值区间是[-2147483648,-2080374784),我们统计落在该数值区间内哈希值的数量,得到 

分区编号,哈希值数量>

 

数值对,这样就可以绘图了。

分区代码如下:

/**

*将整个哈希空间等分成64份,统计每个空间内的哈希值数量

publicstaticMap<

Integer,Integer>

partition(List<

//step=2^32/64=2^26

finalintstep=67108864;

List<

nums=newArrayList<

>

();

Map<

statistics=newLinkedHashMap<

intstart=0;

for(longi=Integer.MIN_VALUE;

=Integer.MAX_VALUE;

i+=step){

finallongmin=i;

finallongmax=min+step;

intnum=(int)hashs.parallelStream()

.filter(x->

=min&

x<

max).count();

statistics.put(start++,num);

nums.add(num);

//为了防止计算出错,这里验证一下

inthashNum=nums.stream().reduce((x,y)->

x+y).get();

asserthashNum==hashs.size();

returnstatistics;

本文中的哈希值是用整形表示的,整形的数值区间是 

[-2147483648,2147483647],区间大小为 

2^32。

所以这里可以将区间等分成64个子区间,每个自子区间大小为 

2^26。

详细的分区对照表如下:

分区编号

分区下限

分区上限

-2147483648

-2080374784

32

67108864

1

-2013265920

33

134217728

2

-1946157056

34

201326592

3

-1879048192

35

268435456

4

-1811939328

36

335544320

5

-1744830464

37

402653184

6

-1677721600

38

469762048

7

-1610612736

39

536870912

8

-1543503872

40

603979776

9

-1476395008

41

671088640

10

-1409286144

42

738197504

11

-1342177280

43

805306368

12

-1275068416

44

872415232

13

-1207959552

45

939524096

14

-1140850688

46

1006632960

15

-1073741824

47

1073741824

16

-1006632960

48

1140850688

17

-939524096

49

1207959552

18

-872415232

50

1275068416

19

-805306368

51

1342177280

20

-738197504

52

1409286144

21

-671088640

53

1476395008

22

-603979776

54

1543503872

23

-536870912

55

1610612736

24

-469762048

56

1677721600

25

-402653184

57

1744830464

26

-335544320

58

1811939328

27

-268435456

59

1879048192

28

-201326592

60

1946157056

29

-134217728

61

2013265920

30

-67108864

62

2080374784

31

63

2147483648

接下来,让我们对照上面的分区表,对数字2、3、17、31、101的散点曲线图进行简单的分析。

先从数字2开始,数字2对于的散点曲线图如下:

上面的图还是很一幕了然的,乘子2算出的哈希值几乎全部落在第32分区,也就是 

[0,67108864)数值区间内,落在其他区间内的哈希值数量几乎可以忽略不计。

这也就不难解释为什么数字2作为乘子时,算出哈希值的冲突率如此之高的原因了。

所以这样的哈希算法要它有何用啊,拖出去斩了吧。

接下来看看数字3作为乘子时的表现:

3作为乘子时,算出的哈希值分布情况和2很像,只不过稍微好了那么一点点。

从图中可以看出绝大部分的哈希值最终都落在了第32分区里,哈希值的分布性很差。

这个也没啥用,拖出去枪毙5分钟吧。

在看看数字17的情况怎么样:

数字17作为乘子时的表现,明显比上面两个数字好点了。

虽然哈希值在第32分区和第34分区有一定的聚集,但是相比较上面2和3,情况明显好好了很多。

除此之外,17作为乘子算出的哈希值在其他区也均有分布,且较为均匀,还算是一个不错的乘子吧。

接下来来看看我们本文的主角31了,31作为乘子算出的哈希值在第33分区有一定的小聚集。

不过相比于数字17,主角31的表现又好了一些。

首先是哈希值的聚集程度没有17那么严重,其次哈希值在其他区分布的情况也要好于17。

总之,选31,准没错啊。

最后再来看看大质数101的表现,不难看出,质数101作为乘子时,算出的哈希值分布情况要好于主角31,有点喧宾夺主的意思。

不过不可否认的是,质数101的作为乘子时,哈希值的分布性确实更加均匀。

所以如果不在意质数101容易导致数据信息丢失问题,或许其是一个更好的选择。

4.写在最后

经过上面的分析与实践,我想大家应该明白了StringhashCode方法中选择使用数字31作为乘子的原因了。

本文本质是一篇简单的科普文而已,并没有银弹。

如果大家读完后觉得又涨知识了,那这篇文章的目的就达到了。

最后,本篇文章的配图画的还是很辛苦的,所以如果大家觉得文章不错,不妨就给个赞吧,就当是对我的鼓励了。

另外,如果文章中有不妥或者错误的地方,也欢迎指出来。

如果能不吝赐教,那就更好了。

最后祝大家生活愉快,再见。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 能源化工

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1