再谈hash函数

前面查看了一下一些经典的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; 
}








你可能感兴趣的:(再谈hash函数)