参考文章: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,函数返回的数字,即数组下标不能过大。否则在实际应用中很难实现那么大的数组。
因此哈希表中存储的数据即不能过于集中(这样会大大增加冲突的几率),也不能过于分散(这样往往会导致数组过大,内存难以承受)。
一般来说,哈希表的容量就应该为数据大小的一倍。当容量过小时,往往哈希表会出现急剧的性能下降。
解决冲突的方法
冲突是不可避免的。因此设计哈希算法时,必须考虑如何解决冲突。为了避免冲突,方式有开放地址法和链接地址法。
1,开放地址法
首先看两个概念:
一串连续的已填充的数组单元叫填充序列。当填充序列变的越来越长时,叫聚集。
前面已经提到,哈希表设计的大小一般为需要存储数据的1倍,也就是说,有一半的数组元素不会被填充。
开放地址法就是,所以当冲突发生时,将冲突的数据放到空的数组单元而不再使用下标。
线性探索 -- 当冲突发生时,顺序的将数据放入下一个空的数组单元。每次探测的距离为一个步长,即顺序的探测下个数组单元。
二次探索 -- 然而线性探索的问题是容易产生聚集:因为是顺序的放入下个数组单元,因此越来越多的数据会被放置在连续的数字单元,这样当冲突发生时,需要继续向后探索的数组元素就越来越多,导致性能下降。
而二次探索不再按一个步长寻找空的数组单元,而是使用一个步长序列,如1,3,5...即第一次探测下个数组单元,如果还是冲突,则向后移动3个数组单元,如果还是冲突,则向后移动5个数组单元...直到找到一个空的数字单元为止。
但二次探测的问题是会产生二次聚集:
二次聚集产生的原因是二次探测的算法产生的探测步长总是固定的。
因此比线性探索和二次探索更好的方式是再哈希法。即用另外一个哈希函数来计算关键字,得到一个不同的步长。然后用这个步长来探索数据。但是为了保证一定能找到空的数组单元放置数据,应该和线性探索结合使用。
2,链接地址法
将冲突的元素都放在一个数组元素里,只不过这个元素存的是包含所有冲突数据形成的一个链表。在数组比较空的情况下,链接地址法会让哈希表的性能低一些。因为对于存储在链表中的数据,查找起来麻烦一些。
开放地址发和链接地址法的性能比较
如果在创建哈希表时,需要填入的数据量已知,则开放地址法效果会好一些。由于开放地址法在填入的数据超过50%时,性能会出现急剧下降,因此如果数据量未知,链接地址法会好一些。链接地址法不会出现性能的急剧下降,因为它没有冲突,聚集等问题。
常用哈希算法
哈希算法的好坏和要存储的数据息息相关。不同的哈希函数适用于不同类型的数据,没有一种算法是万能的。
先来看两个概念:
Perfect hashing--完美哈希。即所有的input都会被映射到唯一的hash值,没有冲突。这种哈希函数当然是很理想化的了,如果对于一类数据能有一个perfect hashing存在,那么毫无以为这个函数就是对于这类数据最好的哈希算法。
对于完美哈希算法,With such a function one can directly locate the desired entry in a hash table, without any additional searching.
http://en.wikipedia.org/wiki/Perfect_hash_function
Minimal perfect hashing -- 最小完美哈希。就是将n个数据,正好映射到0~n-1个数组单元的哈希算法。没有冲突,也没有浪费的数组单元。
1,Trivial hash function -- 最简单的哈希算法
当数据本身是数字且范围很小时,就是用数据本身作为哈希值就可以了。例如要存储的数据是2,663,30,0,87,...,376,那么就用数据本身作为数组下标。
或者数据是file001.txt, file002.txt 等等这样时,可以将数字部分取出,然后对数据总数取余作为数组下标。
2,对于那些长短不一的,分布不均的字符串
例如网址,邮箱地址,甚至是一段文字等数据。需要一个hash function that depends on all characters of the string—and depends on each character in a different way.
一般首先需要将数据按字母,或单词,或独特的分隔符拆分成小的单元。然后对每个小单元应用算法。之后再将结果组合在一起,最终输出哈希值。
Merkle–Damgård construction 就是个常用的算法。
更多的算法参考原文吧。