长按图片识别二维码报名参与码云用户见面交流会
一、数组是什么?
忘了在哪本书里曾看到过类似这样的一句话“所有的数据结构都是数组的演化”,想想其实是有道理的,因为计算机的内存其实就是线性的存储空间。
这样的数据结构可以很方便地通过数组下标存取数据,但在查找时需要遍历数组,平均时间复杂度为O(n/2)。
当数据量很大或者查找操作频繁的时候,这样的遍历操作几乎是不可接受的。那么,如何才能够在更短的时间内快速找到数据呢?
二、二分查找
假如数组内的元素是已经排序的,那么很自然的方式就是使用二分查找。
三、懵懂的思考
还记得刚学编程不久时看《编程珠玑》,其中有一段描述:20世纪70年代,Mike Lesk为电话公司做了电话号码查找功能,譬如输入LESK*M*对应的数字键5375*6*,可以返回正确的电话号码或可选列表,错误匹配率仅0.2%。
加法
所有数字相加(*键为11),5375*6*的和为48。大多数输入的和不超过100,意味着几万个电话号码都会聚集在数组前100的位置,所以是不可行的。
乘法
所有数字相乘,积为381150。这似乎是可行的,但出现了这样的问题:lesk、lsek、slke……的积是一样的,每一个数字键还对应着3个字母,意味着重复的概率会很高。
改进后的乘法
①字母相同但字母序不同的字符串,可以通过这样的方式来避免冲突:每一个数字先乘以序号,然后再与其它值相乘。 ②每一个数字键对应了3个英文字母,如果用户没有输入字母在数字键中的序号,是没办法再进一步精确计算的。能考虑的方向只能是根据给定的单词表来做概率统计,所以不予考虑。
位置冲突
即使用改进后的乘法,不同的姓名字母计算得出的积依然还是可能会发生重复,那么当发生冲突后应该怎么办?
数组太大
唯一的问题是,按照上述思路建立的数组实在是太大了。一个姓名有10个字母,假如每一个字母对应的数字都是9,最后得到的积约是9的17次方。这意味着要建立9^17大小的数组,这是完全不可行的。
后来
即使不考虑数组过大因素,以我当时初学编程的水平,这样的程序也是没有能力写出来的。
四、HashMap结构及算法详解
HashMap的本质依然是数组,而且是一个不定长的多维数组,近似于下图这样的结构:
简单介绍
HashMap中的数组保存链表的头节点。保存数据:查找数据:注意事项:
下面分别详细介绍这三个因素的算法设计。
hashcode生成
这是String类的hashCode生成代码。
String类的value是char[],char可以转换成UTF-8编码。譬如,’a’、’b’、’c’的UTF-8编码分别是97,98,99;“abc”根据上面的代码转换成公式就是:
如上图所示,字符串末尾每3个字母就会产生一个重复的hashcode。这并不是一个巧合,即使换成其它的英文字母,也有很容易产生重复,而使用质数则会大大地减少重复可能性。有兴趣的可以参照上图去作一下左位移运算,会发现这并不是偶然。
哈希函数设计
现在,已经得到了重复率很低的hashCode,还有什么美中不足的地方吗?
⑴ 扰动函数
下图还是以字符串“abcabcabcabcabc”为例,使用上面方法得到的运算过程和结果。
为什么要先无符号右位移16位,然后再执行异或运算?看看下图的二进制的与运算,你就会明白。
你会发现只要二进制数后4位不变,前面的二进制位无论如何变化都会出现相同的结果。为了防止出现这种高位变化而低位不变导致的运算结果相同的情况,因此需要将高位的变化也加入进来,而将整数的二进制位上半部与下半部进行异或操作就可以得到这个效果。为什么要使用与运算?
⑵ 源代码详解
代码解释之前需要补充说明一下,jdk1.8引入了红黑树来解决大量冲突时的查找效率,所以当一个链表中的数据大到一定规模的时候,链表会转换成红黑树。因此在jdk1.8中,HashMap的查找和保存数据的最大时间复杂度实际上就是红黑树的时间复杂度O(logN)。
以下是HashMap中的保存数据的方法源代码,相信经过以上的描述,已经非常容易看懂这一段代码。
⑶ 简化后的伪代码
⑷ 链表大小设计
代码注释中已经稍有提及,这里再分别展开讨论。①容量选择
根据公式index = hashCode % size可知,无论size是质数还是非质数,index的值区间都是0至(size-1)之间,似乎真正起决定作用的是hashCode的随机性要好。
这里先不下结论,待以后再写一个随机函数比较下质数和非质数重复率。②装填因子
关于其它
红黑树是在JDK 1.8中引入的,想用简单的语言来讲清楚红黑树的数据结构、增删改查操作及时间复杂度分析,这是一个复杂且艰难的任务,更适合单独来描述,因此留待以后吧。
五、最小完美哈希函数
Jdk中的HashMap解决了任意数据集的时间复杂度问题,所设计的哈希函数在未知数据集的情况下有良好的表现。
系统环境:参考资料: