你有一个非常大的字符串数组A,现在又有一个字符串B,需要你去检测B是否存在于A中。最简单粗暴的方法是遍历整个A,但是这个方法投入到实际应用时的运行速度是难以接受的。在没有与其他所有字符串比较前怎么知道该字符串是否存在呢?
解决方法是使用哈希表,即用较小的数据类型来代表较大的数据类型,例如:用数字来代表字符串。你可以存储哈希值与字符串一一对应,当需要检测一个字符串时,就用哈希算法计算其哈希值,然后与存储的哈希值比较级可以得出结果,使用这一方法根据数组的大小和字符串长度提升速度大约100倍。
unsigned long HashString(char *lpszString)
{
unsigned long ulHash = 0xf1e2d3c4;
while (*lpszString != )
{
ulHash <<= ;
ulHash += *lpszString++;
}
return ulHash;
}
上述代码演示了一个简单的哈希算法。该函数在遍历整个字符串时,将ulHash左移一位再叫上字符值。使用这个算法 ,"arr\units.dat" 的哈希值是0x5A858026,字符串"unit\neutral\acritter.grp" 的哈希值是0x694CD020。但是这个算法没有什么使用价值,因为它产生的哈希值是可以预测的,可能使不同的字符产生相同的哈希值,从而产生碰撞。
要解决这一问题方法,网上流传最神的是MPQ,源自于暴雪公司的文件打包管理,用于Blizzard游戏的数据文件,包括图形,声音,等级等数据,该算法能够压缩,解密,文件分割等功能。详情见维基:http://en.wikipedia.org/wiki/MPQ。
该算法产生的哈希值完全无法预测,非常高效,被称为"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)。
unsigned long HashString(char *lpszFileName, unsigned long dwHashType)
{
unsigned char *key = (unsigned char *)lpszFileName;
unsigned long seed1 = 0x7FED7FED, seed2 = 0xEEEEEEEE;
int ch; while(*key != )
{
ch = toupper(*key++);
seed1 = cryptTable[(dwHashType << ) + ch] ^ (seed1 + seed2);
seed2 = ch + seed1 + seed2 + (seed2 << ) + ;
}
return seed1;
}
尝试在前面的示例中使用相同索引,您的程序一定会有中断现象发生,而且不够快。如果想让它更快,您能做的只有让程序不去查询数组中的所有散列值。或者您可以只做一次对比就可以得出在列表中是否存在字符串。听起来不错,真的么?不可能的啦
一个哈希表就是以字符串的哈希值作为下标的一类数组。我的意思是,哈希表使用一个固定长度的字符串数组(比如1024,2的偶次幂)进行存储;当你要看看这个字符串是否存在于哈希表中,为了获取这个字符串在哈希表中的位置,你首先计算字符串的哈希值,然后哈希表的长度取模。这样如果你像上一节那样使用简单的哈希算法,字符串"arr\units.dat" 的哈希值是0x5A858026,偏移量0x26(0x5A858026 除于0x400等于0x16A160,模0x400等于0x26)。因此,这个位置的字符串将与新加入的字符串进行比较。如果0X26处的字符串不匹配或不存在,那么表示新增的字符串在数组中不存在。下面是示意的代码:
int GetHashTablePos(char *lpszString, SOMESTRUCTURE *lpTable, int nTableSize)
{
int nHash = HashString(lpszString), nHashPos = nHash % nTableSize;
if (lpTable[nHashPos].bExists && !strcmp(lpTable[nHashPos].pString, lpszString))
return nHashPos;
else
return -; //Error value
}
上面的说明中存在一个缺陷。当有冲突(两个不同的字符串有相同的哈希值)发生的时候怎么办?显而易见的,它们不能占据哈希表中的同一个位置。通常的解决办法是为每一个哈希值指向一个链表,用于存放所有哈希冲突的值;
MPQs使用一个存放文件名的哈希表来跟踪文件内部,但是表的格式与通常方法有点不同,首先不像通常的做法使用哈希值作为偏移量,存储实际的文件名。MPQs 根本不存储文件名,而是使用了三个不同的哈希值:一个用做哈希表偏移量,两个用作核对。这两个核对的哈希值用于替代文件名。当然从理论上说存在两个不同的文件名得到相同的三个哈希值,但是这种情况发送的几率是:1:18889465931478580854784,这应该足够安全了。
MPQ's的哈希表的实现与传统实现的另一个不同的地方是,相对与传统做法(为每个节点使用一个链表,当冲突发生的时候,遍历链表进行比较),看一下下面的示范代码,在MPQ中定位一个文件进行读操作:
int GetHashTablePos(char *lpszString, MPQHASHTABLE *lpTable, int nTableSize)
{
const int HASH_OFFSET = , HASH_A = , HASH_B = ;
int nHash = HashString(lpszString, HASH_OFFSET),nHashA = HashString(lpszString, HASH_A),nHashB = HashString(lpszString, HASH_B), nHashStart = nHash % nTableSize,nHashPos = nHashStart;
while (lpTable[nHashPos].bExists)
{
if (lpTable[nHashPos].nHashA == nHashA && lpTable[nHashPos].nHashB == nHashB)
return nHashPos; else
nHashPos = (nHashPos + ) % nTableSize;
if (nHashPos == nHashStart)
break;
}
return -; //Error value }
无论代码看上去有多么复杂,其背后的理论并不难。读一个文件的时候基本遵循下面这样一个过程:
1、计算三个哈希值(一个哈希偏移量和两个验证值)并保存到变量中;
2、移动到哈希偏移量对应的值;
3、对应的位置是否尚未使用?如果是,则停止搜寻,并返回"文件不存在";
4、这两个验证值是否与我们要找的字符串验证值匹配,如果是,停止搜寻,并返回当前的节点;
5、移动到下一个节点,如果到了最后一个节点则返回开始;
6、如果移动到了相同的偏移值(遍历了整个哈希表),则停止搜寻,并返回"文件不存在";
7、回到第3步;
如果你注意的话,可能已经从我们的解释和示例代码注意到,MPQ的哈希表已经将所有的文件入口放入MPQ中;那么当哈希表的每个项都被填充的时候,会发生什么呢?答案可能会让你惊讶:你不能添加任何文件。有些人可能会问我为什么文件数量上有这样的限制(文件限制),是否有办法绕过这个限制?就此而言,如果不重新创建MPQ 的项,甚至无法调整哈希表的大小。这是因为每个项在哈希表中的位置会因为跳闸尺寸而改变,而我们无法得到新的位置,因为这些位置值是文件名的哈希值,而我们根本不知道文件名是什么。
如果想要深入了解MPQ入此坑: http://sfsrealm.hopto.org/inside_mopaq/index.htm