您的位置:首页 > 其它

哈希理论

2016-05-26 13:39 225 查看

计算哈希值方法

哈希场景

哈希算法有两个评价标准,一个是无法回源,一个是随机性(碰撞概率小),一个是计算速度。不同的应用环境对这几个目的的需求是不一样的。例如文件的md5计算和签名算法,无法回源与随机性都需要。但是哈希表的数据结构在使用的时候,主要看重随机性和计算速度。例如下载一个文件md5与一个病毒的md5一样,这时候下载器就认为你下载了正确的文件,就会带来严重的安全问题,这时其对哈希算法的要求在随机性上市最苛刻的。
哈希表很多使用场景下对速度的要求最苛刻,为此甚至可以牺牲一定的随机性。在计算机范围内,随机性也意味着空间使用效率。所以,计算机科学的哈希表,基本上是随机性和计算速度的一个折中平衡。计算机的哈希表也一般不会使用md5,sha等签名算法。这里主要讨论计算机哈希查找表的哈希算法。

哈希表的应用场景

哈希表服务于快速添加和查找的,他能最快速的添加和查找。哈希表中可以携带value,也可以不带。不带value的应用场景有很多,例如只是为了确认一个ip是否在,所以就可以直接使用一个位来表示。但是哈希表有冲突的可能,所以即使计算到同一个位也不一定代表相同的记录。所以一般的做法要在每个哈希值上开多个存储标记,叫做bucket(4是个很好的数字)。
但是也有应用场景是不需要开bucket的,冲突也无所谓,只是需要一个将大取值空间的值变为一个小取值空间的值,例如cache,如果冲突了就回写就好了。
还有一种应用是merkle数,用以使用哈希函数比较两份备份存储系统的文件一致性。
哈希表还可以用来检查两个输入之间相似的程度,如果足够相似就会落在同一个哈希位置。所以很多字符纠错,或者是音频指纹,甚至字符串匹配(Rabin–Karp algorithm)等可以使用。
还可以使用rolling hash进行字符串的子串查找。把这个思路用bloom filter一扩展就可以查找多模式匹配。
哈希还可以用来做压缩。因为我们的哈希拥有将距离比较近的值变为一个值得能力,这也正是压缩所需要的。
还可以设计可以动态调整大小的哈希函数,这种哈希的输入有哈希表的大小。这个的显著好处是哈希表在快要满的时候仍然可以使用哈希函数的效率来拓展空间。但是每次拓展空间可能要面临原有哈希的重新计算或排布(有比较好的算法)
哈希的应用有很多,无论何种应用,最核心就是将大范围的数如何平均的,随机的,快速的变为小范围的数。

固定哈希表的哈希函数

我们工程中遇到的最多的情况是固定大小哈希表的快速查找和添加。哈希函数有很多,这里有一个比较全面的列表:
https://en.wikipedia.org/wiki/List_of_hash_functions
但是实际在工程中可以方便使用的不多。

1,除法散列法
最直观的一种,上图使用的就是这种散列法,公式:
index = value % 16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。这就是相当于求哈希表大小的同余。结果是连续的,随机性不强,但是比较抗碰撞。
2,平方散列法
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
index = (value * value) >> 28 右移,除以2^28。记法:左移变大,是乘。右移变小,是除。
如果数值分配比较均匀的话这种方法能得到不错的结果,但我上面画的那个图的各个元素的值算出来的index都是0——非常失败。也许你还有个问题,value如果很大,value * value不会溢出吗?答案是会的,但我们这个乘法不关心溢出,因为我们根本不是为了获取相乘结果,而是为了获取index。
3,斐波那契(Fibonacci)散列法
平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。
对我们常见的32位整数而言,公式:
index = (value * 2654435769) >> 28
如果用这种斐波那契散列法的话,那上面的图就变成这样了:



注:用斐波那契散列法调整之后会比原来的取摸散列法好很多。
适用范围
快速查找,删除的基本数据结构,通常需要总数据量可以放入内存。
基本原理及要点
hash函数选择,针对字符串,整数,排列,具体相应的hash方法。

4. Crc。Intel的cpu内置这种crc指令,计算起来非常快。所以在硬件支持的情况下可以使用这个得到最好的效果。
5. Jhash。内核中也有,dpdk也有,效果和速度都中上,但是不需要使用硬件加速。
6. Pearson's hash适用于不支持shift和xor操作的情况
7. 黄金数。Linux内核里面使用32位和64位的黄金数,经测试效果是一流的好。
8. Toeplitz。这种哈希一般用在网络协议栈中(linux内核中就有)做receive side scaling,就是选择把数据包放到哪个cpu去执行。
9. Xor。效果一般,但是很简单。不过用在非线性表的时候效果非常好也很快(用空间换时间)。非线性表哈希是使用一个随机的数字表,可以接收任意长度的输入,每次输入都是截取固定的长度(具体取决于表的大小),在一个输入的多次输入的索引结果进行xor就可以得到最后的哈希值。
10. 利用外部的随机性直接做哈希索引。例如远端的ip地址的最后几位。
有太多种的哈希算法,使用哈希的时候一定要根据自己的应用场景而合理的选择,以上列举的都是比较通用的做法。例如crc也可以用来计算字符串,只要将其看作数字序列。有调查表明,使用64位的系统使用这种方案可以把速度提高四五倍

哈希的冲突避免

哈希表对于冲突的处理,总的来说有三种:不需要解决碰撞问题的,允许丢失数据的,不允许丢失数据的。不解决碰撞问题的(例如cache),冲突避免算法就不需要,允许丢失数据的,冲突避免算法也很简单,就是每个哈希值开固定长度的bucket,满了就丢。不允许丢失数据的哈希表需要设置告警,因为哈希严重冲突的时候哈希表可能就没有意义了。
冲突避免算法主要讨论不允许丢失数据的情况。主要有两种思路:开链表和利用已有的静态分配的空间(cuckoo)。

Cuckoo hashing

在冲突域比较大的情况下可能会造成性能急剧下降。并且每一个哈希的entry都存储了4个字节的key签名,所以当哈希表比较大的时候,这个表浪费非常严重。并且每次要计算两次哈希函数。这是dpdk实现的cuckoo 哈希的测试结果。



所以这个算法要尽量的分配更大的表大小,使用尽量小才能保证高效。Zukowski et al.做了研究证明Cuckoo hashing比chain类型的哈希在表小的时候效率更高。

拉链法

碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。

d-left hashing

中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同 时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。

所以d left的时候可能就需要对应的查找多个位置,增加复杂度,但是提高防冲突能力。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: