链表查找的时间效率为O(N),二分法为log2N,B+ Tree为log2N,但Hash链表查找的时间效率为O(1)。 |
2 经典字符串Hash函数介绍 |
作者阅读过大量经典软件原代码,下面分别介绍几个经典软件中出现的字符串Hash函数。 |
2.1 PHP中出现的字符串Hash函数 |
static unsigned long hashpjw(char *arKey, unsigned int nKeyLength) |
{ |
unsigned long h = 0, g; |
char *arEnd=arKey+nKeyLength; |
while (arKey < arEnd) { |
h = (h << 4) + *arKey++; |
if ((g = (h & 0xF0000000))) { |
h = h ^ (g >> 24); |
h = h ^ g; |
} |
} |
return h; |
} |
2.2 OpenSSL中出现的字符串Hash函数 |
unsigned long lh_strhash(char *str) |
{ |
int i,l; |
unsigned long ret=0; |
unsigned short *s; |
if (str == NULL) return(0); |
l=(strlen(str)+1)/2; |
s=(unsigned short *)str; |
for (i=0; i |
ret^=(s[i]<<(i&0x0f)); |
return(ret); |
} */ |
/* The following hash seems to work very well on normal text strings |
* no collisions on /usr/dict/words and it distributes on %2^n quite |
* well, not as good as MD5, but still good. |
*/ |
unsigned long lh_strhash(const char *c) |
{ |
unsigned long ret=0; |
long n; |
unsigned long v; |
int r; |
if ((c == NULL) || (*c == '\0')) |
return(ret); |
/* |
unsigned char b[16]; |
MD5(c,strlen(c),b); |
return(b[0]|(b[1]<<8)|(b[2]<<16)|(b[3]<<24)); |
*/ |
n=0x100; |
while (*c) |
{ |
v=n|(*c); |
n+=0x100; |
r= (int)((v>>2)^v)&0x0f; |
ret=(ret(32-r)); |
ret&=0xFFFFFFFFL; |
ret^=v*v; |
c++; |
} |
return((ret>>16)^ret); |
} |
在下面的测量过程中我们分别将上面的两个函数标记为OpenSSL_Hash1和OpenSSL_Hash2,至于上面的实现中使用MD5算法的实现函数我们不作测试。 |
2.3 MySql中出现的字符串Hash函数 |
#ifndef NEW_HASH_FUNCTION |
/* Calc hashvalue for a key */ |
static uint calc_hashnr(const byte *key,uint length) |
{ |
register uint nr=1, nr2=4; |
while (length--) |
{ |
nr^= (((nr & 63)+nr2)*((uint) (uchar) *key++))+ (nr << 8); |
nr2+=3; |
} |
return((uint) nr); |
} |
/* Calc hashvalue for a key, case indepenently */ |
static uint calc_hashnr_caseup(const byte *key,uint length) |
{ |
register uint nr=1, nr2=4; |
while (length--) |
{ |
nr^= (((nr & 63)+nr2)*((uint) (uchar) toupper(*key++)))+ (nr << 8); |
nr2+=3; |
} |
return((uint) nr); |
} |
#else |
/* |
* Fowler/Noll/Vo hash |
* |
* The basis of the hash algorithm was taken from an idea sent by email to the |
* IEEE Posix P1003.2 mailing list from Phong Vo (kpv@research.att.com) and |
* Glenn Fowler (gsf@research.att.com). Landon Curt Noll (chongo@toad.com) |
* later improved on their algorithm. |
* |
* The magic is in the interesting relationship between the special prime |
* 16777619 (2^24 + 403) and 2^32 and 2^8. |
* |
* This hash produces the fewest collisions of any function that we've seen so |
* far, and works well on both numbers and strings. |
*/ |
uint calc_hashnr(const byte *key, uint len) |
{ |
const byte *end=key+len; |
uint hash; |
for (hash = 0; key < end; key++) |
{ |
hash *= 16777619; |
hash ^= (uint) *(uchar*) key; |
} |
return (hash); |
} |
uint calc_hashnr_caseup(const byte *key, uint len) |
{ |
const byte *end=key+len; |
uint hash; |
for (hash = 0; key < end; key++) |
{ |
hash *= 16777619; |
hash ^= (uint) (uchar) toupper(*key); |
} |
return (hash); |
} |
#endif |
Mysql中对字符串Hash函数还区分了大小写,我们的测试中使用不区分大小写的字符串Hash函数,另外我们将上面的两个函数分别记为MYSQL_Hash1和MYSQL_Hash2。 |
2.4 另一个经验字符串Hash函数 |
unsigned int hash(char *str) |
{ |
register unsigned int h; |
register unsigned char *p; |
for(h=0, p = (unsigned char *)str; *p ; p++) |
h = 31 * h + *p; |
return h; |
} |
3 测试及结果 |
3.1 测试说明 |
从上面给出的经典字符串Hash函数中可以看出,有的涉及到字符串大小敏感问题,我们的测试中只考虑字符串大小写敏感的 函数,另外在上面的函数中有的函数需要长度参数,有的不需要长度参数,这对函数本身的效率有一定的影响,我们的测试中将对函数稍微作一点修改,全部使用长 度参数,并将函数内部出现的计算长度代码删除。 |
我们用来作测试用的Hash链表采用经典的拉链法解决冲突,另外我们采用静态分配桶(Hash链表长度)的方法来构造Hash链表,这主要是为了简化我们的实现,并不影响我们的测试结果。 |
测试文本采用单词表,测试过程中从一个输入文件中读取全部不重复单词构造一个Hash表,测试内容分别是函数总调用次 数、函数总调用时间、最大拉链长度、平均拉链长度、桶利用率(使用过的桶所占的比率),其中函数总调用次数是指Hash函数被调用的总次数,为了测试出函 数执行时间,该值在测试过程中作了一定的放大,函数总调用时间是指Hash函数总的执行时间,最大拉链长度是指使用拉链法构造链表过程中出现的最大拉链长 度,平均拉链长度指拉链的平均长度。 |
测试过程中使用的机器配置如下: |
PIII600笔记本,128M内存,windows 2000 server操作系统。 |
3.2 测试结果 |
以下分别是对两个不同文本文件中的全部不重复单词构造Hash链表的测试结果,测试结果中函数调用次数放大了100倍,相应的函数调用时间也放大了100倍。 |
|
|
常用的字符串Hash函数还有ELFHash,APHash等等,都是十分简单有效的方法。这些函数使用位运算使得每一个字符都对最后的函数值产生影响。另外还有以MD5和SHA1为代表的杂凑函数,这些函数几乎不可能找到碰撞。
常用字符串哈希函数有BKDRHash,APHash,DJBHash,JSHash,RSHash,SDBMHash,PJWHash,ELFHash等等。对于以上几种哈希函数,我对其进行了一个小小的评测。
Hash函数 | 数据1 | 数据2 | 数据3 | 数据4 | 数据1得分 | 数据2得分 | 数据3得分 | 数据4得分 | 平均分 |
BKDRHash | 2 | 0 | 4774 | 481 | 96.55 | 100 | 90.95 | 82.05 | 92.64 |
APHash | 2 | 3 | 4754 | 493 | 96.55 | 88.46 | 100 | 51.28 | 86.28 |
DJBHash | 2 | 2 | 4975 | 474 | 96.55 | 92.31 | 0 | 100 | 83.43 |
JSHash | 1 | 4 | 4761 | 506 | 100 | 84.62 | 96.83 | 17.95 | 81.94 |
RSHash | 1 | 0 | 4861 | 505 | 100 | 100 | 51.58 | 20.51 | 75.96 |
SDBMHash | 3 | 2 | 4849 | 504 | 93.1 | 92.31 | 57.01 | 23.08 | 72.41 |
PJWHash | 30 | 26 | 4878 | 513 | 0 | 0 | 43.89 | 0 | 21.95 |
ELFHash | 30 | 26 | 4878 | 513 | 0 | 0 | 43.89 | 0 | 21.95 |
其中数据1为100000个字母和数字组成的随机串哈希冲突个数。数据2为100000个有意义的英文句子哈希冲突个数。数据3为数据1的哈希值与 1000003(大素数)求模后存储到线性表中冲突的个数。数据4为数据1的哈希值与10000019(更大素数)求模后存储到线性表中冲突的个数。
经过比较,得出以上平均得分。平均数为平方平均数。可以发现,BKDRHash无论是在实际效果还是编码实现中,效果都是最突出的。APHash也 是较为优秀的算法。DJBHash,JSHash,RSHash与SDBMHash各有千秋。PJWHash与ELFHash效果最差,但得分相似,其算 法本质是相似的。
在信息修竞赛中,要本着易于编码调试的原则,个人认为BKDRHash是最适合记忆和使用的。
BYVoid原创,欢迎建议、交流、批评和指正。
附:各种哈希函数的C语言程序代码: |