前面查看了一下一些经典的hash算法,要看看猛戳这里
在前面查找算法里面都是需要遍历整个数据集,有没有一种方法直接可以将数据映射成一个唯一的地址,然后在进行查找的时候,直接通过映射关系一步到位就可以找到数据。这样的话就需要使用一种技术叫做散列技术,或许好多人都听过这个词儿hash。
所谓的散列技术就是如前面所想的那样,记录的存储位置与关键字之间有一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key),这里的f叫做hash函数。利用散列技术将记录存储在一块连续的存储空间中,这样的空间称为hash表,java或者c++中有实现叫做hash_map。
写到这里意识到一个重要点就是,怎么找到这个映射关系,也就是这个hash函数。设计hash函数的时候要遵循一下几点:
1、简单;
2、分布均匀;
3、存储利用率高;
4、尽量减少冲突。
对这个冲突(collision),要做一下说明,理想的hash函数就是一个关键字对应于一个唯一的地址,但是实际的情况是不同的关键字可以有相同的地址,这种情况就是冲突或者叫碰撞,这两个关键字或者多个关键字叫做同义词。
看了这四个原则,可能有点晕哈~该怎么设计才能满足这几点呢?一般有以下几点做到:
直接定地法、数字分析法、平方取中法、折叠法等方法过于局限,不做讨论。现在主要看一下除留余数法,hash值的计算公式如下:
f(key)=key mod p (p<=m)
这里的p为要取值的模大小,m为散列表长。这里模值p的取值决定了hash函数产生hash值的好坏。通常p的取值为最接近散列表表长的质数,或者是不包含小于20质因子的合数。这里看一个这样的例子:
unsigned int hash(char *p,int hashlen) { unsigned int h = 0; for (; *p; p++) h = 31 *h + *p; return h % hashlen; }下面要说的问题就是碰撞问题(冲突问题),有下面几种方式进行处理。
首先是开放地址法:
这个思路很简单,一旦出现冲突就寻找下一个空的散列地址,这样的话只要你的散列表足够大,总能找到存储的位置。
这里使用线性探测法进行元素的存储:
fh(key)=(f(key)+d[i]) mod m (d[i]=1,2,3,...,m-1)上面的方法虽然可以满足,但是这样可能会造成地址的浪费,有些散列表的地址没有使用。比如有次f(key)=5的时候出现了冲突,但是4位置没有存储任何数据,从5到m-1均有有数据存在,这样的话就出现了无地方可存储数据的情况,可能又会开辟一块区域进行存储此数据。
为了解决上面的问题我们引入二次探测法,d[ i ]的取值为-q的平方与q的平方,这里q的范围为(q<=m/2)。故而公式改进为:
fh(key)=(f(key)+d[i]) mod m (-1,1,-4,4,......)这里采用平方的目的是为了使数据更加稀疏地分布在散列表上,而不是聚集在一块地方。
然后还有一种解决方法是链地址法:
这种方法,散列表中只存储所有同义词的头结点,对于冲突的同义词,将其存储到此hash表链表中。这样处理就不会存在所谓的地址冲突的烦恼了,但是引入了链表的遍历。java与C++的hash_map就是采用这种方式。
最后一种为公共溢出法:
这种方法需要建立两个表,一个用以存储无冲突的元素,另一个存储冲突的元素。思路很简单~
下面是从源代码中扣出来的几个hash函数,分别是php,openssl,mysql内的实现:
unsigned long php_hash(char *arKey) { unsigned long h = 0, g; unsigned int nKeyLength=strlen(arKey); char *arEnd=arKey+nKeyLength; while(arKey<arEnd) { h = (h << 4) + *arKey++; if((g = (h & 0xF0000000))) { h = h^(g>>24); h = h^g; } } return h; } unsigned long openssl1_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<l;i++) ret^=(s[i]<<(i&0x0f)); return ret; } unsigned long openssl2_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 << r) | (ret >> (32 - r)); ret &= 0xFFFFFFFFL; ret ^= v * v; c++; } return ((ret >> 16) ^ ret); } static unsigned int mysql_hash1(const char *key) { unsigned int length=strlen(key); register unsigned int nr=1, nr2=4; while (length--) { nr^= (((nr & 63)+nr2)*((unsigned int) (unsigned char) *key++))+ (nr << 8); nr2+=3; } return((unsigned int) nr); } unsigned int mysql_hash2(const char *key) { unsigned int len=strlen(key); const char *end=key+len; unsigned int hash; for (hash = 0; key < end; key++) { hash *= 16777619; hash ^= (unsigned int) (unsigned char) toupper(*key); } return hash; }