應(yīng)用Hash函數(shù)
應(yīng)用Hash函數(shù)作者:沖處宇宙
時(shí)間:2007.1.25
計(jì)算理論中,沒(méi)有Hash函數(shù)的說(shuō)法,只有單向函數(shù)的說(shuō)法。所謂的單向函數(shù),是一個(gè)復(fù)雜的定義,大家可以去看計(jì)算理論或者密碼學(xué)方面的數(shù)據(jù)。用“人類(lèi)”的語(yǔ)言描述單向函數(shù)就是:如果某個(gè)函數(shù)在給定輸入的時(shí)候,很容易計(jì)算出其結(jié)果來(lái);而當(dāng)給定結(jié)果的時(shí)候,很難計(jì)算出輸入來(lái),這就是單項(xiàng)函數(shù)。各種加密函數(shù)都可以被認(rèn)為是單向函數(shù)的逼近。Hash函數(shù)(或者成為散列函數(shù))也可以看成是單向函數(shù)的一個(gè)逼近。即它接近于滿(mǎn)足單向函數(shù)的定義。
Hash函數(shù)還有另外的含義。實(shí)際中的Hash函數(shù)是指把一個(gè)大范圍映射到一個(gè)小范圍。把大范圍映射到一個(gè)小范圍的目的往往是為了節(jié)省空間,使得數(shù)據(jù)容易保存。除此以外,Hash函數(shù)往往應(yīng)用于查找上。所以,在考慮使用Hash函數(shù)之前,需要明白它的幾個(gè)限制:
1. Hash的主要原理就是把大范圍映射到小范圍;所以,你輸入的實(shí)際值的個(gè)數(shù)必須和小范圍相當(dāng)或者比它更小。不然沖突就會(huì)很多。
2. 由于Hash逼近單向函數(shù);所以,你可以用它來(lái)對(duì)數(shù)據(jù)進(jìn)行加密。
3. 不同的應(yīng)用對(duì)Hash函數(shù)有著不同的要求;比如,用于加密的Hash函數(shù)主要考慮它和單項(xiàng)函數(shù)的差距,而用于查找的Hash函數(shù)主要考慮它映射到小范圍的沖突率。
應(yīng)用于加密的Hash函數(shù)已經(jīng)探討過(guò)太多了,在作者的博客里面有更詳細(xì)的介紹。所以,本文只探討用于查找的Hash函數(shù)。
Hash函數(shù)應(yīng)用的主要對(duì)象是數(shù)組(比如,字符串),而其目標(biāo)一般是一個(gè)int類(lèi)型。以下我們都按照這種方式來(lái)說(shuō)明。
一般的說(shuō),Hash函數(shù)可以簡(jiǎn)單的劃分為如下幾類(lèi):
1. 加法Hash;
2. 位運(yùn)算Hash;
3. 乘法Hash;
4. 除法Hash;
5. 查表Hash;
6. 混合Hash;
下面詳細(xì)的介紹以上各種方式在實(shí)際中的運(yùn)用。
一 加法Hash
所謂的加法Hash就是把輸入元素一個(gè)一個(gè)的加起來(lái)構(gòu)成最后的結(jié)果。標(biāo)準(zhǔn)的加法Hash的構(gòu)造如下:
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是任意的質(zhì)數(shù),看得出,結(jié)果的值域?yàn)閇0,prime-1]。
二 位運(yùn)算Hash
這類(lèi)型Hash函數(shù)通過(guò)利用各種位運(yùn)算(常見(jiàn)的是移位和異或)來(lái)充分的混合輸入元素。比如,標(biāo)準(zhǔn)的旋轉(zhuǎn)Hash的構(gòu)造如下:
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);
}
先移位,然后再進(jìn)行各種位運(yùn)算是這種類(lèi)型Hash函數(shù)的主要特點(diǎn)。比如,以上的那段計(jì)算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
這種類(lèi)型的Hash函數(shù)利用了乘法的不相關(guān)性(乘法的這種性質(zhì),最有名的莫過(guò)于平方取頭尾的隨機(jī)數(shù)生成算法,雖然這種算法效果并不好)。比如,
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類(lèi)的hashCode()方法也使用乘法Hash。不過(guò),它使用的乘數(shù)是31。推薦的乘數(shù)還有:131, 1313, 13131, 131313等等。
使用這種方式的著名Hash函數(shù)還有:
// 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;
}
以及改進(jìn)的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;
}
除了乘以一個(gè)固定的數(shù),常見(jiàn)的還有乘以一個(gè)不斷改變的數(shù),比如:
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算法的應(yīng)用沒(méi)有CRC32廣泛,不過(guò),它可能是乘法Hash里面最有名的一個(gè)了。關(guān)于它的介紹,大家可以去看RFC 1950規(guī)范。
四 除法Hash
除法和乘法一樣,同樣具有表面上看起來(lái)的不相關(guān)性。不過(guò),因?yàn)槌ㄌ@種方式幾乎找不到真正的應(yīng)用。需要注意的是,我們?cè)谇懊婵吹降膆ash的結(jié)果除以一個(gè)prime的目的只是為了保證結(jié)果的范圍。如果你不需要它限制一個(gè)范圍的話(huà),可以使用如下的代碼替代”hash%prime”: hash = hash ^ (hash>>10) ^ (hash>>20)。
五 查表Hash
查表Hash最有名的例子莫過(guò)于CRC系列算法。雖然CRC系列算法本身并不是查表,但是,查表是它的一種最快的實(shí)現(xiàn)方式。下面是CRC32的實(shí)現(xiàn):
static int crctab[256] = {
省略
};
int crc32(String key, int hash)
{
int i;
for (hash=key.length(), i=0; i<key.length(); ++i)
hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ k.charAt(i)];
return hash;
}
查表Hash中有名的例子有:Universal Hashing和Zobrist Hashing。他們的表格都是隨機(jī)生成的。
六 混合Hash
混合Hash算法利用了以上各種方式。各種常見(jiàn)的Hash算法,比如MD5、Tiger都屬于這個(gè)范圍。它們一般很少在面向查找的Hash函數(shù)里面使用。
七 對(duì)Hash算法的評(píng)價(jià)
http://www.burtleburtle.net/bob/hash/doobs.html 這個(gè)頁(yè)面提供了對(duì)幾種流行Hash算法的評(píng)價(jià)。我們對(duì)Hash函數(shù)的建議如下:
1. 字符串的Hash。最簡(jiǎn)單可以使用基本的乘法Hash,當(dāng)乘數(shù)為33時(shí),對(duì)于英文單詞有很好的散列效果(小于6個(gè)的小寫(xiě)形式可以保證沒(méi)有沖突)。復(fù)雜一點(diǎn)可以使用FNV算法(及其改進(jìn)形式),它對(duì)于比較長(zhǎng)的字符串,在速度和效果上都不錯(cuò)。
2. 長(zhǎng)數(shù)組的Hash。可以使用http://burtleburtle.net/bob/c/lookup3.c這種算法,它一次運(yùn)算多個(gè)字節(jié),速度還算不錯(cuò)。
八 后記
本文簡(jiǎn)略的介紹了一番實(shí)際應(yīng)用中的用于查找的Hash算法。Hash算法除了應(yīng)用于這個(gè)方面以外,另外一個(gè)著名的應(yīng)用是巨型字符串匹配(這時(shí)的Hash算法叫做:rolling hash,因?yàn)樗仨毧梢詽L動(dòng)的計(jì)算)。設(shè)計(jì)一個(gè)真正好的Hash算法并不是一件容易的事情。做為應(yīng)用來(lái)說(shuō),選擇一個(gè)適合的算法是最重要的。
posted on 2008-08-02 11:40 anyStar 閱讀(1168) 評(píng)論(0) 編輯 收藏