前言
最近在看HashMap的时候,比较吃力。在网上找了很多博主写的博客,依然没有把HashMap搞明白,反而搞的有点一知半解。
正如有句英文格言说:“A little knowledge is dangerous.”。一知半解的状态是“危险的”。
我继续在Google上查资料,了解相关知识。最后,我发现我方向错了。
原来我一直都是使用诸如“HashMap 原理”、“HashMap 内部机制”、“HashMap 详解”这些关键词作为搜索条件。搜索到的文章有些写的非常详细,分析的很到位,但我总是感觉哪里读不明白。
当我阅读到源码putVal()方法时,我注意到“哈希碰撞(hash collisions)”这个短语。我这才意识到,问题出在我缺少一些非常重要的背景知识上:
- 什么是hash?
- 什么是hash table?
- 什么是hash functions?
- 什么是hash collisions?
这才明白我需要先去补充背景知识,才能对理解HashMap有帮助。
于是我在Google搜索框打下了关键词:What is hash collision?
经过一番搜索,我找到了一篇文章,帮助我理解了上面那些问题。
以下内容整理自文章:散列函数及其在密码学中的应用 Hashing Functions and Their Uses In Cryptography (密苏里大学圣路易斯分校官网)
正文
哈希函数和哈希
- 哈希函数
定义:哈希函数是一个函数,它接收一组任意大小的输入,并将其放入一个包含固定大小元素的表或其他数据结构中。
- 哈希
定义:哈希是由用于生成该特定表或数据结构的哈希函数生成的表或数据结构中的一个值。生成的表或数据结构通常被称为哈希表。一般还认为,访问哈希表中的数据的时间复杂度是O(1),或常数。
计算哈希表
哈希函数的正式定义因应用而异。让我们举个简单的例子,把每个数字的模10,放到一个有10个槽的哈希表里。
需要哈希的数值:22, 3, 18, 29。
我们取每个值,对其应用哈希函数,其结果会告诉我们应该把这个值放在哪个槽里,左栏表示槽,右栏表示这个槽里有什么值,如果有的话。
我们在这里用的哈希函数是对每个值进行模10。下面的表格显示了产生的哈希表。我们在得到一系列数值时对其进行散列,所以我们散列的第一个数值是数值串中的第一个数值,而我们散列的最后一个数值是数值串中的最后一个数值。
22 mod 10 = 2,所以它被放入2号槽。
3 mod 10 = 3,所以它被放入3号槽。
18 mod 10 = 8,所以它被放在8号槽。
29 mod 10 = 9,所以它被放在9号槽里。
碰撞
定义:当一个特定的哈希函数将一个以上的值哈希到由哈希函数生成的表或数据结构(哈希表)中的同一个槽时,就会发生碰撞。
有碰撞的哈希表示例
让我们采取与之前完全相同的哈希函数:取要哈希的数值模10,并将其放入哈希表的对应槽中。
需要哈希的数值:22, 9, 14, 17, 42。
和前面一样,哈希表显示在下面。
和前面一样,我们对出现在要散列的值字符串中的每个值进行哈希,从第一个值开始。前四个值可以毫无问题地输入到哈希表中。然而,最后一个值,即42,却造成了一个问题。42 mod 10 = 2,但在哈希表的第2槽中已经有一个值,即22。这就是一个碰撞。
42这个值最终 必须 出现在哈希表的某个槽中,但任意给它分配一个槽会使访问哈希表中的数据变得更加耗时,因为我们显然想保持访问哈希表的恒定时间增长不变。有两种常见的处理碰撞的方法:链式和开放寻址。
处理碰撞的方法
方法一:链式法处理碰撞
当我们使用链式法来解决碰撞时,我们只是允许哈希表的每个槽接受一个以上的值。因此,在上面的例子中,42将简单地放在槽2中,正如哈希函数告诉我们的那样,放在22之后的列表中。
注:在HashMap中,也是采用类似这种解决方法。其内部使用链表来处理哈希碰撞的问题。(当然,在java1.8之后,又在此基础上进行了改进,增加了红黑树的结构。当链表长度超过一定值时就会将链表转换成红黑树的结构。)
方法二:开放地址法处理碰撞
这部分略,有兴趣的同学可以自行去上面给出的原文链接去查看。