Hash 函数

来源:互联网 发布:1701端口是干嘛的 编辑:程序博客网 时间:2024/06/11 16:49
散列表,它时给予快速存取的角度设计的,也是一种典型的“空间换时间”的做法。顾名思义,该数据可以理解为一个线性表,但是其中的元素不是紧密排列的,而是可能存在空隙。

散列表(Hash table,也叫哈希表),是根据关键码值儿直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置访问记录,以加快查找的速度。这个影色好函数叫做散列函数,存放纪录的数组叫做散列表。

比如我们存储70个元素,但我们可能为70元素申请了100个元素的空间。70/100=0.7,这个数字称为负载因子。
注释:我们之所以这样做,也是为了“快速存取”的目的。

我们基于一种结果尽可能随机平均分布的固定函数H为每个元素安排存储位置,这样可以避免便利性质的先行搜索,以达到快速存取。但是由于此随机性,也必然会导致一个问题就是冲突。所谓冲突,即两个元素通过散裂函数H得到的地址形同,那么这两个元素称为“同义词”。这类似于70个人去一个又100个椅子的饭店吃饭。散裂函数的计算结果是一个存储单位地址,每个存储单位称为“桶”。设一个散裂表有m个桶,则散列表函数的值域应为[0,m-1]。

解决冲突时一个复杂问题。冲突主要取决于:

1)散裂函数,一个好的散列函数的值应尽可能平均分布
2)处理冲突方法
3)负载因子的大小。太大浪费空间严重,负载因子和散列函数是联动的


解决冲突的方法:


1)线性探查法:冲突后,线性向前试探,找到最近的一个空位置。缺点时回出现堆积现象。存取时,可能不是同义词的词也位于探查序列,影响效率。
2)双散列函数:在位置d冲突后,再次使用另一个散列函数产生一个散列桶容量互质的数c,一次试探(d+n*c),时探查序列跳跃式分布。
崇勇的构造散列函数的方法

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快的定位:

1.直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key)=a?key+b,其中a和b常数(这种散列函数叫做自身函数)

2.数字分析法:分析一组数据,比如一组员工的出生年月,这时我们发现出生年月日的前几位数字答题相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后继伟表示月份和具体日期的数字拆憋很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突纪律较低的散列地址。

3.平方取中法:取关键字平方后的中间纪委作为散列地址。

4.折叠法:选择一随机函数,去关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

5.随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。

6.除留余数法:取关键字被某个不大于散列表表长m的数P除后所得的余数为散列地址。即H(key) = key mod p,p<=m.不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的酸则很重要,一般取素数或m,若p选的不好,容易产生同义词。

散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键吗在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理宠的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。


查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,超着效率就低。因此影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:

1.散列函数是否均匀;

2处理冲突的方法;

3散列表的田庄因子。

散列表的装填因子定义为:a=填入表中的元素歌数/散列的长度

a是散列表装满程度的标志因子。由于表长是定值,a与“填入表中的元素个数”成正比,所以,a越大,填入表中的元素越多,产生冲突的可能性就越大;a越小,填入表中的元素较少,产生冲突的可能性就越小。

注:a和表中元素数量小可以减少冲突

实际上,散列表的平均查找长度是装填因子a的函数,只是不同处理冲突的方法有不同的函数。

了解了hash基本定义,就不能不提到一些著名的hash算法,MD5和SHA-1可以说是目前应用足迹广泛的hash算法,而他们都是以MD4为基础设计的。那么他们是什么意思?

eg:
1)MD4(RFC 1320)是MIT 的Ronald L.Rivest 在1990年设计的,MD是Message digest的缩写。它适合在32位字长的处理器上用高速软件实现--它是机遇32位操作来实现的。

2)MD5(RFC 1321)是Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与MD4相同。MD5比MD4来的复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好。

3)SHA-1及其他
SHA1是由NIST NSA 设计为同DSA一起使用的,他对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1设计时基于喝MD4相同原理,并且模仿了该算法。

哈希表不可避免冲突(collision)现象:对不同的关键字可能得到同意哈希地址,即key1!=key2,而hash(key1)=hash(key2)。因此,在建造哈希表时不仅要设定一个好的哈希函数,而且要设定一种处理冲突的方法。可如下描述哈希表:根据设定的哈希函数H(key)和所选中的处理冲突的方法,将一组关键字映像到一个有限的、地址连续的地址集(区间)上并以关键字在地址集中的”象“作为响应纪录在表中的存储位置,这种表被称为哈希表。

对于动态查找表而言,

1)表长不确定;

2)在设计查找表时,只知道关键字所属范围,而不知道确切的关键字。因此,一般情况需建立一个函数关系,以f(key)作为关键字为key的录在表中的位置,通常称这个函数f(key)为哈希函数。(注意:这个函数并不一定是数学函数)

哈希函数是一个映像,即:将关键字的结合影响到某个地址结合上,它的设置很灵活,只要这个地址集合的大小不超过允许范围即可。
现实中哈希函数是需要构造的,并且构造的好才能使用的好。


那么这些hash算法到底有什么用呢?
hash算法在信息安全方面的应用主要体现在以下的3个方面


1)文件校验

我们比较熟悉的娇艳算法有奇偶校验喝CRC娇艳,这两种娇艳并没有抗数据转改能力,他们一定程度上能检测病纠正数据传输中的信道误码,但却不能防止对数据的恶意破坏。

MD5 Hash算法的“数字指纹”特性,使它成为目前应用广泛的一种文件完整性娇艳喝(Checksum)算法,不少Unix系统由提供计算md5 checksum命令。

2)数字签名

Hash算法也是现代密码体系中的一个重要组成部分。由于非对称算法的运行速度较慢,所以在数字签名协议中,单向散列函数扮演了一个重要的角色。对Hash值,又称“数字摘要”进行数字签名,在同济上可以认为与对文件本身进行数字签名是等效的。而且这样的协议还有其他的优点。

3)鉴权协议

如下的鉴权协议又称作挑战--认证模式:在传输信道是可被真听,但不可以被修改的情况下,这是一种简单而安全的方法。

文件hash值

MD5-hash-文件的数字文摘通过Hash函数计算得到。不管文件长度如何,它的Hash函数计算结果是一个固定长度的数字。与机密算法不同,这个Hash算法是一个不可逆的单向函数。采用安全性高的Hash算法,如MD5、 SHA时,两个不同的文件几乎不可能得到相同的Hash结果。因此一旦文件被修改,就可检测出来。

Hash函数还有另外的含义。实际中的Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,是的数据容易保存。除此之外,Hash函数往往应用于查找上。所以,在考虑使用Hash函数之前需要明白几个限制:

1.Hash的主要原理就是把范围映射到小范围;所以,你输入的实际值的个数必须喝小范围相当或者比它更小。不然冲突就会很多;

2.由于Hash逼近单向函数;所以,你可以用它来对数据进行加密;

3.不同的应用对Hash函数有着不同的要求。

eg:用于加密的Hash函数主要考虑它和单向函数的差距,而用于查找的Hash函数主要考虑它映射到小范围的冲突率。


Hash函数应用的主要对象是数组,而其目标一般是个int类型。以下我们都按照这种方式来说明。
一般说,Hash函数可以简单的划分为如下几类:

1.加法Hash;
2.位运算Hash;
3.乘法Hash;
4.除法Hash;
5.查表Hash;
6.混合Hash;


下面详细介绍以上各种方式在实际中的运用。


一 加法Hash 所谓的加法Hash就是把输入元素一个一个的加起来构成最后的结果。标准的加法Hash的构造如下:

static int additiveHash(String key, int prime) {
    int hash, i;
    for (hash = key.length(), i = 0; i < key.length(); i++)
        hash += key.charAt(i); return (hash % prime);
} 这里的prime是任意的质数,看得出,结果的值域为[0,prime-1]。

二 位运算Hash 这类型Hash函数通过利用各种位运算(常见的是移位和异或)来充分的混合输入元素。比如,标准的旋转Hash的构造如下:
static int rotatingHash(String key, int prime) {
    int hash, i;
    for (hash=key.length(), i=0; i<key.length(); ++i)
        hash = (hash<<4)^(hash>>28)^key.charAt(i); return (hash % prime);
}
先移位,然后再进行各种位运算是这种类型Hash函数的主要特点。比如,以上的那段计算hash的代码还可以有如下几种变形:
1. hash = (hash<<5)^(hash>>27)^key.charAt(i);
2. hash += key.charAt(i);
hash += (hash << 10);
hash ^= (hash >> 6);
3. if((i&1) == 0)
{
    hash ^= (hash<<7) ^ key.charAt(i) ^ (hash>>3);
} else {
    hash ^= ~((hash<<11) ^ key.charAt(i) ^ (hash >>5)); }
4. hash += (hash<<5) + key.charAt(i);
5. hash = key.charAt(i) + (hash<<6) + (hash>>16) – hash;
6. hash ^= ((hash<<5) + key.charAt(i) + (hash>>2));


三 乘法Hash


这种类型的Hash函数利用了乘法的不相关性(乘法的这种性质,最有名的莫过于平方取头尾的随机数生成算法,虽然这种算法效 果并不好)。比如,

static int bernstein(String key) {
    int hash = 0;
    int i;
    for (i=0; i<key.length(); ++i) hash = 33*hash + key.charAt(i); return hash;
}
jdk5.0里面的String类的hashCode()方法也使用乘法Hash。不过,它使用的乘数是31。推荐的乘数还有:131, 1313, 13131, 131313等等。
使用这种方式的著名Hash函数还有: // 32位FNV算法
int M_SHIFT = 0;
public int FNVHash(byte[] data) {
    int hash = (int)2166136261L; for(byte b : data)
        hash = (hash * 16777619) ^ b; if (M_SHIFT == 0)
            return hash;
    return (hash ^ (hash >> M_SHIFT)) & M_MASK;
}
以及改进的FNV算法:
public static int FNVHash1(String data) {
    final int p = 16777619;
    int hash = (int)2166136261L; for(int i=0;i<data.length();i++)
        hash = (hash ^ data.charAt(i)) * p; hash += hash << 13;
    hash ^= hash >> 7;
    hash += hash << 3;
    hash ^= hash >> 17; hash += hash << 5; return hash;
}
除了乘以一个固定的数,常见的还有乘以一个不断改变的数,比如: static int RSHash(String str)
{
    int b = 378551; int a = 63689; int hash = 0;
    for(int i = 0; i < str.length(); i++) {
        hash = hash * a + str.charAt(i);
        a =a*b; }
    return (hash & 0x7FFFFFFF);
}
虽然Adler32算法的应用没有CRC32广泛,不过,它可能是乘法Hash里面最有名的一个了。关于它的介绍,大家可以去看RFC 1950规范。


四 除法Hash

除法和乘法一样,同样具有表面上看起来的不相关性。不过,因为除法太慢,这种方式几乎找不到真正的应用。需要注意的是, 我们在前面看到的hash的 结果除以一个prime的目的只是为了保证结果的范围。如果你不需要它限制一个范围的话,可以使用如 下的代码替代”hash%prime”: hash = hash ^ (hash>>10) ^ (hash>>20)。


五 查表Hash

查表Hash最有名的例子莫过于CRC系列算法。虽然CRC系列算法本身并不是查表,但是,查表是它的一种最快的实现方式。查表 Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他们的表格都是随机生成的。


六 混合Hash
混合Hash算法利用了以上各种方式。各种常见的Hash算法,比如MD5、Tiger都属于这个范围。它们一般很少在面向查找的 Hash函数里面使用。


七 对Hash算法的评价
http://www.burtleburtle.net/bob/hash/doobs.html 这个页面提供了对几种流行Hash算法的评价。我们对Hash函数的建议如 下:

1. 字符串的Hash。最简单可以使用基本的乘法Hash,当乘数为33时,对于英文单词有很好的散列效果(小于6个的小写形式可 以保证没有冲突)。复杂一点可以使用FNV算法(及其改进形式),它对于比较长的字符串,在速度和效果上都不错。

2. 长数组的Hash。可以使用http://burtleburtle.net/bob/c/lookup3.c这种算法,它一次运算多个字节,速度还算不错。

八 后记
本文简略的介绍了一番实际应用中的用于查找的Hash算法。Hash算法除了应用于这个方面以外,另外一个著名的应用是巨型字 符串匹配(这时的 Hash算法叫做:rolling hash,因为它必须可以滚动的计算)。设计一个真正好的Hash算法并不是一件容易 的事情。做为应用来说,选择一个适合的算法是最重要的。
九 数组hash
inline int hashcode(const int *v) {
    int s = 0;
    for(int i=0; i<k; i++) s=((s<<2)+(v[i]>>4))^(v[i]<<10); s = s % M;
    s = s < 0 ? s + M : s; return s;
}
注:虽说以上的hash能极大程度地避免冲突,但是冲突是在所难免的。所以无论用哪种hash函数,都要加上处理冲突的方法
























0 0
原创粉丝点击