关于本文《系统学习hash算法》的由来。在看到了《十一、从头到尾彻底解析Hash 表算法》这篇文章之后,原文中没有暴雪hash快的原因分析以及和别的hash方法比能快多少,结合自己以前研究MonetDB的数据库时也涉及到了hash join的相关内容,于是决定实现一个简单的hash和暴雪hash比较下,但在搜索资料时发现了《字符串Hash函数评估》这篇文章,感觉挺有意思就参照着该文章中的源码,写了自己的实验测试程序,发现了问题,下文中详细解释。
在准备实现一个hash算法时需要思考一下三个问题:
第一:hash函数的选择。
第二:hash冲突的解决办法。
第三:装填因子大小的选择。装填因子 a=n / m。其中m为hash表的bucket个数;n为关键字的个数。装填因子越大,产生hash冲突就严重。
对于第一条,一般情况下基本不需要自己考虑,找到别人设计好的,同时也很适合自己的hash函数用用就行了。所以暂不考虑hash函数的设计问题(目前的认识是这样)
对于第二条,书上写了一堆的方法,在实践中见到的最多的就是“拉链法”,暴雪hash中使用的应该叫线性探测再散列法。还有就是再哈希法,布谷鸟hash中使用的应该就是此种方法,孰优孰劣不好说,各有千秋吧,以后用到时会再对比,再分析。
对于第三条,记忆中看那本关于java的教材时,上边说最佳的取值范围是0.75--0.8。不太确定,但是《数据结构》教材上有关于hash算法性能与装填因子a取值的数学推导,后续详细研究。
Hash查找因为其O(1)的查找性能而著称,被对查找性能要求高的应用所广泛采用。它的基本思想是:
(1) 创建一个定长的线性Hash表,一般可以初始化时指定length;
(2) 设计Hash函数,将关键字key散射到Hash表中。其中hash函数设计是最为关键的,均匀分布、冲突概率小全在它;
(3) 通常采用拉链方法来解决hash冲突问题,即散射到同一个hash表项的关键字,以链表形式来表示(也称为桶bucket);
(4) 给定关键字key,就可以在O(1) + O(m)的时间复杂度内定位到目标。其中,m为拉链长度,即桶深。
Hash应用中,字符串是最为常见的关键字,应用非常普通,现在的程序设计语言中基本上都提供了字符串hash表的支持。字符串hash函数非常多,常见的主要有Simple_hash, RS_hash, JS_hash, PJW_hash, ELF_hash, BKDR_hash, SDBM_hash, DJB_hash, AP_hash, CRC_hash等。它们的C语言实现见后面附录代码: hashFunction.c, hashTests.c。那么这么些字符串hash函数,谁好熟非呢?评估hash函数优劣的基准主要有以下两个指标:
(1) 散列分布性
即桶的使用率backet_usage = (已使用桶数) / (总的桶数),这个比例越高,说明分布性良好,是好的hash设计。
(2) 平均桶长
即avg_backet_len,所有已使用桶的平均长度。理想状态下这个值应该=1,越小说明冲突发生地越少,是好的hash设计。
hash函数计算一般都非常简洁,因此在耗费计算时间复杂性方面判别甚微,这里不作对比。
评估方案的设计:
第一步:随机生成1000个字符串,每个字符串的长度均为10。将这1000个 字符串写入一个文件test.txt。做为下一步建立hash表的输入。(生成的字符串的个数由你自己决定)
第二步:分别应用上面提到的各种字符串hash函数,进行hash散列模拟。(注:CRC_hash还没改好)
第三步:统计输出结果,用散列分布性和平均桶长两个指标进行评估分析。(是否能用方差和均方差来评估???,暂存的疑问)
实验的结果如下:
表格中字符串的解释,参见如下注释:
printf("bucket_len = %d\n", pHashTable->mBucketLen); ///哈希表的桶的个数
printf("hit_count = %d\n", hit_count); ///建立hash表的不重复的元素的个数
printf("buket conflict count = %d\n", conflict_count); ///冲突的桶的个数
printf("longest hash entry = %d\n", max_link); ///最长的链的长度
printf("average hash entry length = %.2f\n", avg_link); ///链表的平均长度
printf("backet usage = %.2f%\n", backet_usage); ///hash table的桶的使用率
hash_function_name | bucketcount | bucket_len | hit_count | bucket conflict count | longest hash entry | averge hash entry length | bucket usage | string count |
simple_hash | 1000 | 1000 | 1000 | 264 | 5 | 1.59 | 62.80% | 1000 |
RS_hash | 1000 | 1000 | 1000 | 259 | 5 | 1.58 | 63.20% | 1000 |
JS_hash | 1000 | 1000 | 1000 | 267 | 5 | 1.59 | 62.90% | 1000 |
PJW_hash | 1000 | 1000 | 1000 | 124 | 18 | 8 | 12.5% | 1000 |
ELF_hash | 1000 | 1000 | 1000 | 124 | 18 | 8 | 12.5% | 1000 |
BKDR_hash | 1000 | 1000 | 1000 | 267 | 5 | 1.56 | 63.90% | 1000 |
SDBM_hash | 1000 | 1000 | 1000 | 274 | 5 | 1.59 | 62.70% | 1000 |
DJB_hash | 1000 | 1000 | 1000 | 270 | 6 | 1.57 | 63.50% | 1000 |
AP_hash | 1000 | 1000 | 1000 | 271 | 6 | 1.60 | 62.50% | 1000 |
以上实验结果使用的装填因子是1,装填因子更小些,更能评估不同hash函数散列结果的好坏。
实验结果中,PJW_hash和ELF_hash函数的实验结果很差。
以上hash函数的由来很感兴趣,有待挖掘下!!!,看到的朋友如果了解这些函数的来源请告知,谢谢!
另:以上结果如有异议,请留言多多指教,谢谢。
以下是实验的源代码:
第一部分:字符串随机生成代码(注:此部分也是改编网络上某个哥们儿的代码,一时找不到出处了,谁看到了请告知,我添加上引用,感谢尊重他人劳动成果):
#include
#include
#include
#include
#include
#define STRINGSIZE 10
#define STRINGCOUNT 1000
//如果是在一个程序的循环中不断调用这个函数,那么是没有效果的虽然也是使用的系统的时间函数来初始化随机数发生器,但程序的
//执行速度太快了,可能执行1000次循环返回的秒数都是一样的time返回时间戳
/*
void get_rand_str(char s[],int num)
{
//定义随机生成字符串表
char *str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int i,lstr;
lstr = strlen(str);//计算字符串长度
srand((unsigned int)time((time_t *)NULL));//使用系统时间来初始化随机数发生器
for(i = 0; i < num-2; i++) //按指定大小返回相应的字符串
{
s[i]=str[(rand()%lstr)];
}
s[i++]='\n';
s[i]='\0';
printf("%s",s);
}
*/
int main()
{
FILE *fp1; //定义文件流指针,用于打开读取的文件
char text[10]; //定义一个字符串数组,用于存储读取的字符
int i=0,j=0,lstr;
char *str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
lstr = strlen(str);//计算字符串长度
fp1 = fopen("d:\\test.txt","r+");//只读写方式打开文件a.txt
//while(fgets(text,1024,fp1)!=NULL)//逐行读取fp1所指向文件中的内容到text中
srand((unsigned int)time((time_t *)NULL));//使用系统时间来初始化随机数发生器
for(j=0;j
第二部分:字符串hash函数评估代码:
hashFunction.c
#include
#include
#include "hashTest.h"
/* A Simple Hash Function */
unsigned int simple_hash(char *str)
{
register unsigned int hash;
register unsigned char *p;
for(hash = 0, p = (unsigned char *)str; *p ; p++)
hash = 31 * hash + *p;
return (hash & 0x7FFFFFFF);
}
/* RS Hash Function */
unsigned int RS_hash(char *str)
{
unsigned int b = 378551;
unsigned int a = 63689;
unsigned int hash = 0;
while (*str)
{
hash = hash * a + (*str++);
a *= b;
}
return (hash & 0x7FFFFFFF);
}
/* JS Hash Function */
unsigned int JS_hash(char *str)
{
unsigned int hash = 1315423911;
while (*str)
{
hash ^= ((hash << 5) + (*str++) + (hash >> 2));
}
return (hash & 0x7FFFFFFF);
}
/* P. J. Weinberger Hash Function */
unsigned int PJW_hash(char *str)
{
unsigned int BitsInUnignedInt = (unsigned int)(sizeof(unsigned int) * 8);
unsigned int ThreeQuarters = (unsigned int)((BitsInUnignedInt * 3) / 4);
unsigned int OneEighth = (unsigned int)(BitsInUnignedInt / 8);
unsigned int HighBits = (unsigned int)(0xFFFFFFFF) << (BitsInUnignedInt - OneEighth);
unsigned int hash = 0;
unsigned int test = 0;
while (*str)
{
hash = (hash << OneEighth) + (*str++);
if ((test = hash & HighBits) != 0)
{
hash = ((hash ^ (test >> ThreeQuarters)) & (~HighBits));
}
}
return (hash & 0x7FFFFFFF);
}
/* ELF Hash Function */
unsigned int ELF_hash(char *str)
{
unsigned int hash = 0;
unsigned int x = 0;
while (*str)
{
hash = (hash << 4) + (*str++);
if ((x = hash & 0xF0000000L) != 0)
{
hash ^= (x >> 24);
hash &= ~x;
}
}
return (hash & 0x7FFFFFFF);
}
/* BKDR Hash Function */
unsigned int BKDR_hash(char *str)
{
unsigned int seed = 131; // 31 131 1313 13131 131313 etc..
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
/* SDBM Hash Function */
unsigned int SDBM_hash(char *str)
{
unsigned int hash = 0;
while (*str)
{
hash = (*str++) + (hash << 6) + (hash << 16) - hash;
}
return (hash & 0x7FFFFFFF);
}
/* DJB Hash Function */
unsigned int DJB_hash(char *str)
{
unsigned int hash = 5381;
while (*str)
{
hash += (hash << 5) + (*str++);
}
return (hash & 0x7FFFFFFF);
}
/* AP Hash Function */
unsigned int AP_hash(char *str)
{
unsigned int hash = 0;
int i;
for (i=0; *str; i++)
{
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5)));
}
}
return (hash & 0x7FFFFFFF);
}
/* CRC Hash Function */
/*
unsigned int CRC_hash(char *str)
{
unsigned int nleft = strlen(str);
unsigned long long sum = 0;
unsigned short int *w = (unsigned short int *)str;
unsigned short int answer = 0;
// Our algorithm is simple, using a 32 bit accumulator (sum), we add
// sequential 16 bit words to it, and at the end, fold back all the
// carry bits from the top 16 bits into the lower 16 bits.
while ( nleft > 1 ) {
sum += *w++;
nleft -= 2;
}
//mop up an odd byte, if necessary
if ( 1 == nleft ) {
*( unsigned char * )( &answer ) = *( unsigned char * )w ;
sum += answer;
}
// add back carry outs from top 16 bits to low 16 bits
// add hi 16 to low 16
sum = ( sum >> 16 ) + ( sum & 0xFFFF );
// add carry
sum += ( sum >> 16 );
// truncate to 16 bits
answer = ~sum;
return (answer & 0xFFFFFFFF);
}
*/
hashTests.c
#include
#include
///do not know where use those head file
///#include
///#include
///#include
///#include
#include
#include "hashTest.h"
//#include "md5.h"
#define STRING_LEN 255
///one atom of the chain when build the hash table
struct AtomOfBucketChain {
unsigned char *pKey;
struct AtomOfBucketChain *pNext;
};
struct ChainOfHashTable {
unsigned int mHitCount;
unsigned int mEntryCount;
struct AtomOfBucketChain *pKeys;
};
struct HashTable {
unsigned int mBucketLen;
struct ChainOfHashTable *pTable;
};
unsigned int (*pHashFunc)(char *str);
///choose which hash function to be used
void chooseHashFunc(char *pHashFuncName)
{
if (0 == strcmp(pHashFuncName, "simple_hash"))
pHashFunc = simple_hash;
else if (0 == strcmp(pHashFuncName, "RS_hash"))
pHashFunc = RS_hash;
else if (0 == strcmp(pHashFuncName, "JS_hash"))
pHashFunc = JS_hash;
else if (0 == strcmp(pHashFuncName, "PJW_hash"))
pHashFunc = PJW_hash;
else if (0 == strcmp(pHashFuncName, "ELF_hash"))
pHashFunc = ELF_hash;
else if (0 == strcmp(pHashFuncName, "BKDR_hash"))
pHashFunc = BKDR_hash;
else if (0 == strcmp(pHashFuncName, "SDBM_hash"))
pHashFunc = SDBM_hash;
else if (0 == strcmp(pHashFuncName, "DJB_hash"))
pHashFunc = DJB_hash;
else if (0 == strcmp(pHashFuncName, "AP_hash"))
pHashFunc = AP_hash;
// else if (0 == strcmp(pHashFuncName, "CRC_hash"))
// pHashFunc = CRC_hash;
else
pHashFunc = NULL;
}
///build the hash table
void buildHashTable(unsigned char *pKey, struct HashTable *pHashTable)
{
unsigned int mHashValue = pHashFunc(pKey) % pHashTable->mBucketLen;
struct AtomOfBucketChain *p=NULL;
p = pHashTable->pTable[mHashValue].pKeys;
while(p)
{
if (0 == strcmp(pKey, p->pKey))
{
break;
}
p = p->pNext;
}
if (p == NULL)
{
p = (struct AtomOfBucketChain *)malloc(sizeof(struct AtomOfBucketChain));
if (p == NULL)
{
printf("malloc in buildHashTable filled");
return ;///must have 'return',否则失败也不会停止。
}
p->pKey = strdup(pKey);
p->pNext = pHashTable->pTable[mHashValue].pKeys;
pHashTable->pTable[mHashValue].pKeys = p;
pHashTable->pTable[mHashValue].mEntryCount++;
}
pHashTable->pTable[mHashValue].mHitCount++;
}
///initial hash table
void hashTableInit(struct HashTable *pHashTable)
{
unsigned int i;
if ((NULL == pHashTable) || (NULL==pHashTable->pTable))
{
printf("hashTableInit: malloc pHashTable or pTable failed");
return;
}
for (i = 0; i < pHashTable->mBucketLen; i++)
{
pHashTable->pTable[i].mHitCount=0;
pHashTable->pTable[i].mEntryCount=0;
pHashTable->pTable[i].pKeys=NULL;
}
}
///free space hash table used
void freeHashTable(struct HashTable *pHashTable)
{
unsigned int i;
struct AtomOfBucketChain *pFront, *pBack;
if ((NULL == pHashTable) || (NULL==pHashTable->pTable))
{
printf("hash table has been free");
return;
}
for (i = 0; i < pHashTable->mBucketLen; i++)
{
pFront = pHashTable->pTable[i].pKeys;
while(pFront)
{
pBack = pFront->pNext;
if (pFront->pKey) free(pFront->pKey);
free(pFront);
pFront = pBack;
}
}
free(pHashTable->pTable);
}
///显示统计结果
void showTestsResult(struct HashTable *pHashTable)
{
int backet = 0, sum = 0;
unsigned i=0, max_link=0;
int conflict_count = 0, hit_count = 0;
double avg_link, backet_usage;
for(i = 0; i < pHashTable->mBucketLen; i++)
{
if (pHashTable->pTable[i].mHitCount > 0)
{
backet++;
sum += pHashTable->pTable[i].mEntryCount;
if (pHashTable->pTable[i].mEntryCount > max_link)
{
max_link = pHashTable->pTable[i].mEntryCount;
}
if (pHashTable->pTable[i].mEntryCount > 1)
{
conflict_count++;
}
hit_count += pHashTable->pTable[i].mHitCount;
}
}
backet_usage = backet/1.0/pHashTable->mBucketLen * 100;
avg_link = sum/1.0/backet;
printf("bucket_len = %d\n", pHashTable->mBucketLen); ///哈希表的桶的个数
/// printf("hash_call_count = %d/n", hash_call_count); ///建立hash表的字符串的个数
printf("hit_count = %d\n", hit_count); ///建立hash表的不重复的元素的个数
printf("buket conflict count = %d\n", conflict_count); ///冲突的桶的个数
printf("longest hash entry = %d\n", max_link); ///最长的链的长度
printf("average hash entry length = %.2f\n", avg_link); ///链表的平均长度
printf("backet usage = %.2f%\n", backet_usage); ///hash table的桶的使用率
}
//
void usage()
{
printf("Usage: hash_func_name [backet_len]\n");
printf("hash_func_name:\n");
printf("/tsimple_hash\n");
printf("/tRS_hash\n");
printf("/tJS_hash\n");
printf("/tPJW_hash\n");
printf("/tELF_hash\n");
printf("/tBKDR_hash\n");
printf("/tSDBM_hash\n");
printf("/tDJB_hash\n");
printf("/tAP_hash\n");
// printf("/tCRC_hash\n");
}
int main(int argc, char *argv[])
{
FILE *fp;
int mStringCount=0;
unsigned char pKey[10];
struct HashTable *pHashTable=NULL;
///参数输入
char hashfunctionname[10],bucketcount[10];
printf("input hashfunctionname\n");
gets(hashfunctionname);
printf("input bucketcount\n");
gets(bucketcount);
pHashTable=(struct HashTable*)malloc(sizeof(struct HashTable));
if(NULL==pHashTable)
{
printf("malloc hash table filled");
return -1;
}
/*
if (argc<=1)
{
usage();
return -1;
}
if (2==argc)
{
usage();
}
*/
// pHashTable->mBucketLen = atoi(argv[1]);
pHashTable->mBucketLen = atoi(bucketcount);
pHashTable->pTable=(struct ChainOfHashTable*)malloc(sizeof(struct ChainOfHashTable) * pHashTable->mBucketLen);
if (!(fp = fopen("d:\\test.txt", "r"))) ///假设文件已经生成,需要补充自动生成字符串的函数。将生成的字符串保存在一个文件中。
{
printf("open source file filled");
return -1;
}
hashTableInit(pHashTable);
//chooseHashFunc(argv[0]);
chooseHashFunc(hashfunctionname);
while(fgets(pKey,10,fp)!=NULL)//逐行读取fp1所指向文件中的内容到text中
{
mStringCount++;
buildHashTable(pKey,pHashTable);
}
fclose(fp);
showTestsResult(pHashTable);
printf("String Count: %d",mStringCount); ///建立hash表的字符串的个数
freeHashTable(pHashTable);
return 0;
}
三,暴雪hash的实现及和上述hash函数的对比及分析
四,装填因子和hash算法性能优劣的数学推导。
五,布谷鸟hash算法的实现及讨论。
六,第二部分中的字符串hash函数的由来。
2014年7月6日
未完待续……。