参考文章:http://en.wikipedia.org/wiki/Hash_function
来看这样一个例子。我们有这样一个电话簿:
人名 电话
Emily 010-789987
Smith 010-765443
...
这个表很大,我们希望有一种数据结构用于存储电话号码,并实现
1,当我们想找到一个人(key)对应的电话号码时,需要花费的时间是O(1).
2,希望删除一个人的电话时,需要的时间是O(1).
3,插入一个人的电话时,需要的时间是O(1).
也就是说这些操纵的时间都是常量,和数据结构里已经存了多少数据没有关系。
哈希表的特点
哈希表就是这样的一种数据结构。哈希表是用数组实现的,数组的特点是一旦建立就不可改变大小。
如果不需要有序遍历数据,并且可以提前预测数据量的大小,那么哈希表在速度和易用性方面是无与伦比的。
不论有多少数据,哈希表插入和删除只需要O(1)的时间。
哈希函数
前面的需求意味着:当我们拿到一个人名,也就是key,可以通过key的值直接计算出对应电话在数组中存储的位置(也就是数组元素的下标)。更直白一点:当我们拿到emily这个名字时,我们就能计算出电话存储在数组的,比如,第18个元素中。
因此我们需要一个函数,这个函数的输入是emily,输出是数组下标18(一个数字)。这个函数称为哈希函数。
当我们需要将数据插入哈希表时,使用哈希函数就知道应该插入到数组的什么地方去。
查找某个数据时,使用哈希函数就知道对应的数据在数组中的位置。
删除时使用哈希函数找到数据的位置,然后删除对应的数组元素。
因此实现哈希表,关键就是设计一个好的哈希函数,或称为哈希算法。
哈希函数的实现
y = f(x)
对应例子就是 f("emily") = 18
因此哈希函数需要解决的问关键题是:
1,不同的人名(key),应该尽可能的映射到不同的数组下标。如果一个以上的key映射到了同一个数组位置,称为冲突。
2,函数返回的数字,即数组下标不能过大。否则在实际应用中很难实现那么大的数组。
因此哈希表中存储的数据即不能过于集中(这样会大大增加冲突的几率),也不能过于分散(这样往往会导致数组过大,内存难以承受)。
一般来说,哈希表的容量就应该为数据大小的一倍。当容量过小时,往往哈希表会出现急剧的性能下降。
解决冲突的方法
Hash冲突是不可避免的。因此设计哈希算法时,必须考虑如何解决冲突。避免冲突的方法有很多,这里只介绍最简单的链接地址法。
将冲突的元素都放在一个数组元素里,只不过这个元素存的是包含所有冲突数据形成的一个链表,称为链接地址法。例如对于电话号码:
"Emily": 010-789987
"Smith": 010-765443
f("Emily") = 18,且 f("Tom") = 18,发生了hash碰撞。此时将Emily和Tom的号码放在一个链表里,且存储在A(18)中。但是如何知道同一链表中的哪个号码才是Emily的呢?因此实际链表中存储的不仅仅是号码(value),同时也包含人名(key)。实际上在数组和链表中存储都不仅仅只是value,而且将key也存储进去。
解决哈希值过大的方法
常用方法就是将hash值对数组长度取模,这样就保证数组下标永远不会超过数组长度。问题是可能会导致hash碰撞加剧。