从作用上来讲,构建哈希表的目的是把搜索的时间复杂度降低到O(1),考虑到一个长度为n的序列,如果依次去比较进行搜索的话,时间复杂度是θ(n),或者对其先进行排序然后再搜索会更快一些,但这两种方法都不是最快的方法。
第一个话题:
计算机里面所有存储的内容都是数字,因此我们研究对数字构建哈希表就够了。先来考虑一下,一个好的哈希函数H需要哪些特点:
1.哈希函数的产生的键值要尽可能的均匀,不要出现聚集效应,也就是产生的各种h(k)要尽量的等概率的分布到哈希表的m个槽里去,当然,如果已经知道了输入的类型,我们可以设计出比较好的哈希函数,但是每一个哈希函数都有可能遇到一个特别针对他的输入,以至于所有计算的健值都指向同一个槽。
(一个哈希函数是否均匀的定义:x,y是两个不同的健值,哈希表的长度是m,P{h(x) = h(y)} =1/m )
2.哈希函数本身不能太复杂,以至于计算的时间过长。
例,一些简单的哈希函数:
1.直接哈希 h(k)=k,不会发生碰撞,但是占用空间大,没有意义
2.除法哈希 h(k)=k mod m ,m的取值很有讲究,不能去2和10的幂,这样很多内容都被mod掉了,而且不能取得太小等等等,可以考虑去一个合适的质数。由于计算机里经常用2和10的幂,不是很好用质数,而且还是除法,所以这种哈希效率也不是很高
3.乘法哈希,假设所有的key都是整数,m=2^r ,计算机字长是w,那么构建h(k)=(A*k mod 2^w) rsh (w-r) 其中rsh是右移的意思,A的大小是2^(w-1)<A<2^w 这个哈希函数的好处是,最后的取得h(k)实际上和每一位上的k值都相关,而A和2^w这两个数是互质的,所以想象一个轮盘,周长是2幂,A肯定不是周长的倍数,k是转了多少圈,那么最后的h(k)就会有可能落到轮盘的任意位置。
第二个话题:
就像之前提到的,无论设计一个怎样的哈希函数,碰撞都难以避免,那么如何来解决碰撞的问题呢?主要有以下两种方法:
1.链接法,每一次碰撞都添加一个链表,这样做会增加哈希表的大小,最坏的情况会导致所有的值都指向同一个槽,然后哈希表变成了一个链表,我们的查询也变成了链表的查询。
2.开放寻址法,在不增加哈希表容量的情况下,继续对该表进行“探测”,直到找到一个空位置把内容放进去。 wikipedia里面对此的解释是这样的(Open addressing, or closed hashing, is a method of collision resolution in hash tables. With this method a hash collision is resolved by probing)
分析第一种方法——链表法:
在最坏的情况下,那就是所有的h(k)都指向了同一个槽,那么哈希表实际上就是一个链表,在链表中查询一个值的时间复杂度是θ(n),在最好的情况下,没有发生碰撞那么时间为θ(1)。定义α=n/m为哈希表的装载因子,一次成功的搜索平均用时θ(1+α/2)1表示计算H的时间,α/2表示在链表中所用的平均时间,所以如果n=O(m)那么α就是常数,在这个哈希表中搜索的时间就为θ(1),同时,考虑平均情况下的最坏情况的搜索,时间为θ(1+α),
分析第二种方法——“开放寻址”(封闭哈希)
这种方法主要通过“探寻”来在哈希表中寻找下一个空位置,把值存进去,查询的时候也采用同样的方法,一步一步查找到目标键值。
探寻的方法有:
1.线性探寻
2.非线性探寻
3.双重哈希探寻
4.伪随机序列探寻
这些方法都有一定的局限性,有可能造成*或者次级聚集
现在来分析开放寻址的效率,首先给出理论:对于一个开放寻址的哈希表,α=n/m<1,那么一次不成功搜索的预期探寻次数为1/(1-α).
由此可见,如果α=50% 那么预期探寻次数为2,如果α=90%,预期探寻次数将会显著升高到10,因此在这种策略下,α的大小至关重要(联想到同一天生日的问题,也是这个道理),在工程上某些采用此策略的哈希表会强制α小于75%,如果超过这个值会自动扩充哈希表。
预期探寻次数1/(1-α)是怎样算出来的,如下:
1.首先,查询一个值至少需要1次探寻
2.有n/m的可能性会发生碰撞,我们需要第二次探寻
3.有(n-1)/(m-1)的可能性第二次探寻也发生了碰撞
……
观察到(n-i)/(m-i)<α i=1,2,3……n