China Open source community
站内导航:
站内排行前50热点文章

精华文章  GDB调试精粹及使用实例
普通文章  STL中map用法详解
精华文章  负载均衡软件比较(Hapr...
普通文章  头文件的重复引用
普通文章  递归函数的调用过程
普通文章  TCP三次握手/四次挥手详解
普通文章  贪心策略的理论基础——...
普通文章  BMH算法原理与实现(模...
普通文章  排列组合与回溯算法
普通文章  DP动态规划
精华文章  Android线程模型
普通文章  Linux socket编程之套接字
普通文章  Linux内核中的红黑树
精华文章  linux下使用minicom的几...
普通文章  Java开源Html解析类库
精华文章  enum类型的本质
普通文章  memcached server LRU ...
普通文章  linux设置环境变量的方法
普通文章  android核心模块及相关...
普通文章  linux源代码包(.tar.g...
普通文章  L.A.M.P配置过程
普通文章  在ubuntu9.10下安装QT4...
普通文章  C/C++程序员常见面试题...
普通文章  gcc编译过程概述
普通文章  python的memcache和jso...
普通文章  应用程序二进制接口---ABI
普通文章  linux内核编译问题
普通文章  Java多线程实现简单实例
普通文章  Python程序员常用的IDE...
普通文章  brk和sbrk详述
普通文章  优化C语言代码(程序员必...
普通文章  python非贪婪,多行匹配...
普通文章  函数指针传递和全局指针...
普通文章  Unix操作系统的历史演变
普通文章  网络编程之C10K问题
普通文章  发行版发布:CentOS 5.4
普通文章  在windows中构建gtk开发...
普通文章  i++循环与i--循环的执行...
普通文章  关于Qvariant类--万能的...
普通文章  Debian sudo 设置
普通文章  busybox1.15.x 交叉编译
普通文章  关于僵死进程zombie
普通文章  递归思想的妙用
普通文章  判断链表是否存在环并找...
普通文章  Android Porting Exper...
普通文章  关于/etc/bashrc和$HOM...
普通文章  [翻译]Django初窥
普通文章  Python list的排序
普通文章  Django实现大数据量分页...
普通文章  Debug方式取代printf满...

 
 
 
当前位置: 首页 >> 程序设计 >> 打造最快的Hash表
 
 

打造最快的Hash表

作者:wuliming_sc      来源:http://blog.csdn.net/wuliming_sc     发表时间:2006-11-04     浏览次数:      字号:    

打造最快的Hash

一个简单的问题:有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但...也只能如此了。

最合适的算法自然是使用HashTable(哈希表),先介绍介绍其中的基本知识,所谓Hash,一般是一个整数,通过某种算法,可以把一个字符串"压缩" 成一个整数。当然,无论如何,一个32位整数是无法对应回一个字符串的,但在程序中,两个字符串计算出的Hash值相等的可能非常小,下面看看在MPQ中的Hash算法:

以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500]

void prepareCryptTable()

{

    unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;

 

    for( index1 = 0; index1 < 0x100; index1++ )

    {

        for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 )

        {

            unsigned long temp1, temp2;

 

            seed = (seed * 125 + 3) % 0x2AAAAB;

            temp1 = (seed & 0xFFFF) << 0x10;

 

            seed = (seed * 125 + 3) % 0x2AAAAB;

            temp2 = (seed & 0xFFFF);

 

            cryptTable[index2] = ( temp1 | temp2 );

       }

   }

}

 

 

以下函数计算lpszFileName 字符串的hash值,其中dwHashType hash的类型,在下面GetHashTablePos函数里面调用本函数,其可以取的值为012;该函数返回lpszFileName 字符串的hash值;

 

unsigned long HashString( char *lpszFileName, unsigned long dwHashType )

{

    unsigned char *key  = (unsigned char *)lpszFileName;

unsigned long seed1 = 0x7FED7FED;

unsigned long seed2 = 0xEEEEEEEE;

    int ch;

 

    while( *key != 0 )

    {

        ch = toupper(*key++);

 

        seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2);

        seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;

    }

    return seed1;

}

   Blizzard的这个算法是非常高效的,被称为"One-Way Hash"( A one-way hash is a an algorithm that is constructed in such a way that deriving the original string (set of strings, actually) is virtually impossible)。举个例子,字符串"unitneutralacritter.grp"通过这个算法得到的结果是0xA26067F3


  是不是把第一个算法改进一下,改成逐个比较字符串的Hash值就可以了呢,答案是,远远不够,要想得到最快的算法,就不能进行逐个的比较,通常是构造一个哈希表(Hash Table)来解决问题,哈希表是一个大数组,这个数组的容量根据程序的要求来定义,例如1024,每一个Hash值通过取模运算 (mod) 对应到数组中的一个位置,这样,只要比较这个字符串的哈希值对应的位置又没有被占用,就可以得到最后的结果了,想想这是什么速度?是的,是最快的O(1),现在仔细看看这个算法吧:

 

 

 

typedef struct

{

    int nHashA;

    int nHashB;

    char bExists;

   ......

} SOMESTRUCTRUE;

一种可能的结构体定义?

 

lpszString 为要在hash表中查找的字符串;lpTable 为存储字符串hash值的hash

 

int GetHashTablePos( har *lpszString, SOMESTRUCTURE *lpTable )

{

    int nHash = HashString(lpszString);

    int nHashPos = nHash % nTableSize;

 

    if ( lpTable[nHashPos].bExists  &&  !strcmp( lpTable[nHashPos].pString, lpszString ) )

    {

        return nHashPos;

    }

    else

    {

        return -1; 

    }

}

 

看到此,我想大家都在想一个很严重的问题:“如果两个字符串在哈希表中对应的位置相同怎么办?”,毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用“链表”,感谢大学里学的数据结构教会了这个百试百灵的法宝,我遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。事情到此似乎有了完美的结局,如果是把问题独自交给我解决,此时我可能就要开始定义数据结构然后写代码了。然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串。

MPQ使用文件名哈希表来跟踪内部的所有文件。但是这个表的格式与正常的哈希表有一些不同。首先,它没有使用哈希作为下标,把实际的文件名存储在表中用于验证,实际上它根本就没有存储文件名。而是使用了3种不同的哈希:一个用于哈希表的下标,两个用于验证。这两个验证哈希替代了实际文件名。

当然了,这样仍然会出现2个不同的文件名哈希到3个同样的哈希。但是这种情况发生的概率平均是1:18889465931478580854784,这个概率对于任何人来说应该都是足够小的。现在再回到数据结构上,Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题,看看这个算法:

lpszString 为要在hash表中查找的字符串;lpTable 为存储字符串hash值的hash表;nTableSize hash表的长度;

 

int GetHashTablePos( char *lpszString, MPQHASHTABLE *lpTable, int nTableSize )

{

    const int  HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;

 

    int  nHash = HashString( lpszString, HASH_OFFSET );

    int  nHashA = HashString( lpszString, HASH_A );

    int  nHashB = HashString( lpszString, HASH_B );

    int  nHashStart = nHash % nTableSize;

    int  nHashPos = nHashStart;

 

    while ( lpTable[nHashPos].bExists )

{

/*如果仅仅是判断在该表中时候存在这个字符串,就比较这两个hash值就可以了,不用对

*结构体中的字符串进行比较。这样会加快运行的速度?减少hash表占用的空间?这种

*方法一般应用在什么场合?*/

        if (   lpTable[nHashPos].nHashA == nHashA

&&  lpTable[nHashPos].nHashB == nHashB )

{

  return nHashPos;

}

         else

{

              nHashPos = (nHashPos + 1) % nTableSize;

}

 

          if (nHashPos == nHashStart)

              break;

    }

     return -1;

}

 

 

    1.     计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验)
2. 
察看哈希表中的这个位置

3. 
哈希表中这个位置为空吗?如果为空,则肯定该字符串不存在,返回
4. 
如果存在,则检查其他两个哈希值是否也匹配,如果匹配,则表示找到了该字符串,返回
5. 
移到下一个位置,如果已经移到了表的末尾,则反绕到表的开始位置起继续查询 
6. 
看看是不是又回到了原来的位置,如果是,则返回没找到
7. 
回到3

 

 

  


 

   

 

补充1:其他比较简单一些的hash函数:

 

 

/*key为一个字符串,nTableLength为哈希表的长度

*该函数得到的hash值分布比较均匀*/

unsigned long getHashIndex( const char *key, int nTableLength )

{

    unsigned long nHash = 0;

  

    while (*key)

    {

        nHash = (nHash<<5) + nHash + *key++;

    }

       

    return ( nHash % nTableLength );

}

 

 

补充2

哈希表的数组是定长的,如果太大,则浪费,如果太小,体现不出效率。合适的数组大小是哈希表的性能的关键。哈希表的尺寸最好是一个质数。当然,根据不同的数据量,会有不同的哈希表的大小。对于数据量时多时少的应用,最好的设计是使用动态可变尺寸的哈希表,那么如果你发现哈希表尺寸太小了,比如其中的元素是哈希表尺寸的2倍时,我们就需要扩大哈希表尺寸,一般是扩大一倍。下面是哈希表尺寸大小的可能取值:

17,            37,          79,          163,           331,  

673,           1361,        2729,       471,          10949,        

21911,         43853,      87719,      175447,       350899,

701819,        1403641,   2807303,    5614657,     11229331,   

22458671,     44917381,  89834777,  179669557,   359339171,  

718678369,    1437356741,  2147483647

 

 

 

 

 


 

 

以下为该程序的源代码,在linux下测试通过:

 

#include <stdio.h>


/*crytTable[]里面保存的是HashString函数里面将会用到的一些数据,在prepareCryptTable
 *函数里面初始化*/
unsigned long cryptTable[0x500];


 /***********************************************************
  *以下的函数生成一个长度为0x500(合10进制数:1280)的cryptTable[0x500]
  *
  *
  ***********************************************************/
void prepareCryptTable()
{
    unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i;

    for( index1 = 0; index1 < 0x100; index1++ )
    {
        for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 )
        {
            unsigned long temp1, temp2;

            seed = (seed * 125 + 3) % 0x2AAAAB;
            temp1 = (seed & 0xFFFF) << 0x10;

            seed = (seed * 125 + 3) % 0x2AAAAB;
            temp2 = (seed & 0xFFFF);

            cryptTable[index2] = ( temp1 | temp2 );
       }
   }
}


/***********************************************************
 *以下函数计算lpszFileName 字符串的hash值,其中dwHashType 为hash的类型,
 *在下面GetHashTablePos函数里面调用本函数,其可以取的值为0、1、2;该函数
 *返回lpszFileName 字符串的hash值;
 ***********************************************************/
unsigned long HashString( char *lpszFileName, unsigned long dwHashType )
{
    unsigned char *key  = (unsigned char *)lpszFileName;
unsigned long seed1 = 0x7FED7FED;
unsigned long seed2 = 0xEEEEEEEE;
    int ch;

    while( *key != 0 )
    {
        ch = toupper(*key++);

        seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2);
        seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;
    }
    return seed1;
}

 /***********************************************************
  *在main中测试argv[1]的三个hash值:
  *  ./hash  "arr\units.dat"
  *  ./hash  "unit\neutral\acritter.grp"
  ***********************************************************/
int main( int argc, char **argv )
{
    unsigned long ulHashValue;
    int i = 0;

    if ( argc != 2 )
    {
        printf("please input two arguments\n");
        return -1;
    }

     /*初始化数组:crytTable[0x500]*/
     prepareCryptTable();

     /*打印数组crytTable[0x500]里面的值*/
     for ( ; i < 0x500; i++ )
     {
         if ( i % 10 == 0 )
         {
             printf("\n");
         }

         printf("%-12X", cryptTable[i] );
     }

     ulHashValue = HashString( argv[1], 0 );
     printf("\n----%X ----\n", ulHashValue );

     ulHashValue = HashString( argv[1], 1 );
     printf("----%X ----\n", ulHashValue );

     ulHashValue = HashString( argv[1], 2 );
     printf("----%X ----\n", ulHashValue );

     return 0;
}

编辑 webmaster

 
 
 
评论
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •  
    中国源码网 - www.YuanMa.org - 中国 开放源代码+编程 社区