glibc的字符串哈希算法

时间:2021-09-13 16:45:30

glibc中对于字符串的哈希实现的比较好,首先看一下这个算法的实现函数:

static inline unsigned long int hash_string(const char *str_param)

{

unsigned long int hval, g;

const char *str = str_param;

hval = 0;

while (*str != '/0')

{

hval <<= 4; //右移4位

hval += (unsigned long int) *str++; //新数据叠加到老数据上

g = hval & ((unsigned long int) 0xf << (HASHWORDBITS - 4));//取得叠加后的高4位

if (g != 0)

{

hval ^= g >> (HASHWORDBITS - 8); //将高4位和第一个字节的高4位按位异或

hval ^= g; //新数据的高4位清零,以提高下个循环中的右移效率

}

}

return hval;

}

这个哈希算法相当于一个轮转的算法,按照字符串的从左到右的顺序进行轮转异或,实际上类似于一个回滚加法,就是说一旦左边溢出,那么溢出数据将叠加到最右边,TCP/IP的校验码就是回滚加法计算得到的,这个加法很快很简单,然而这里的哈希算法并不是简单的将一个位回滚到开始的第一个位的位置,而是一共4个位一起回滚到了低字节的高4位,这样的好处就是既使得溢出的字节不丢失,将之异或到低位的某处,又不至于一个位一个位的溢出时效率那么低下,同时又不将它回滚到低4位,而是低8位的高4位,这样能充分降低溢出数据的权值,注意这是在哈希而不是在计算校验值,如果将来取模的话,越是低位的数据影响越大,因此此算法确实是按照按照字符串从左到右的顺序权值依次降低的方式进行扫描和计算的,结论就是字符串后面的字符会比较大的影响哈希结果。对于每一个字符加到已有结果上的时候,必须保证此次循环中这个加上去的新值没有被原来的数据“污染”,因此才会回滚到低8位的高4位。看看hash_string在什么时候使用:

nls_uint32 hash_val = hash_string(const char * str_param, size_t len) (msgid);

nls_uint32 idx = hash_val % domain->hash_size;

nls_uint32 incr = 1 + (hash_val % (domain->hash_size - 2));

果然最终要取模。

哈希算法有两个重要的指标,这就是效率和分布概率,效率当然越高越好,算法越快越好,而分布性当然是分布越均匀越好,times33算法应该是很经典的一个算法了,这个算法很简单,简单意味着效率会很高,起码不会太低,因为没有什么额外的约束需要维护,算法的额外成本几乎为零,那么分布性呢,这就要实践出真知了,一般的观点,一个字符串的哈希就是将所有的字符叠加,但是那太容易碰撞,要知道最终的和越大,碰撞的可能就越大,因为给出一个值,要用加法得到这个值的可能性太多了,给出的数值越大可能性就越多,因此不可取,那么乘法呢?和加法一样,但是比加法要好,主要受到奇数偶数以及质数的限制,因此不能采用连乘的方式,而应该用固定乘数的方式,乘数不能固定乘偶数,因为那样的话,最终的积会有太多的可能性通过乘法得到,要使最终的积不太可能有太多的因数起码要用奇数,现在考虑用什么奇数比较好,当然是因数越少越好了,15,17,31,33,63,65,127,129这些都是奇数,看得出这些数字都是围绕在2的整数次方周围的数字,选这几个数字的好处在于乘法可以归结为一个移位操作和一个加法或者减法操作,效率很高,我们需要结果最好因数越少越好,那么最好的就是质数了,可是这里却不能选择质数,看看这是为什么?因为还要加上一个*str,如果本来就是质数,由于质数本来就少,那么加上随便一个数都可能导致结果不再是质数,而如果本来不是质数,那么加上一个数值后虽然也是很大的概率不是质数,但是还是有那么一点可能是质数的,概率要比前者大一些,这只是一个统计意义上的论述,最终的结果不一定必须是质数,只要保证因数尽可能少就是了,因此实际上不一定是33,129也可以的。最后看一下times33算法的实现:

unsigned int Hash(char *str)

{

unsigned int hash = 5381;

while (*str)

{

hash += (hash << 5) + (*str++);

}

return (hash & 0x7FFFFFFF);

}