暴雪的hash算法[翻译]
原文来自:http://sfsrealm.hopto.org/inside_mopaq/chapter2.htm#hashes
促进历史进步的大多数契机都是在解决特定问题的过程中产生的,本文讨论一下MPQ格式的合适解决方案。
----MPQ是暴雪的一种文本压缩格式,可以压缩包括坐标、算法、声音、动画、字符串等。
HASHS
问题:你可能有一个非常长的字符串数组,现在有一个新字符串,想要判断该字符串是否在数组中,简单粗暴的方法是挨个比较,但最大的弊端就是大部分场合下速度慢到不能忍。
解决方案:hash,hash是一种较小的数据类型,用来替代其他的较大的数据类型(通常是字符串),上述的问题可以存储字符串的hash到数组中,然后比较新字符串的hash是否在数组中,如果数组中的一个hash匹配了新字符串的hash,该字符串就存在于字符串数组中。hash能够将该比较过程提速约100倍,具体的效率提升依赖于数组的长度和字符串的平均长度,下面是一个简单的hash算法
unsigned long HashString(char *lpszString) | ||
unsigned long ulHash = 0xf1e2d3c4; | ||
ulHash <<= 1; | ||
} | ||
} |
MPQ格式使用了非常复杂的hash算法来生成全部不可预期的值,事实上该hash算法是一个单向算法,单向算法是指无法通过hash值反算出原始字符串。下面是这个特别的算法:
unsigned long HashString(char *lpszFileName, unsigned long dwHashType) | ||
unsigned char *key = (unsigned char *)lpszFileName; | ||
ch = toupper(*key++); | ||
} | ||
} |
hash表
问题:你使用一个类似上面例子的索引,但你的程序要求极快的速度,但是索引并没有足够快,你唯一能让索引速度变快的方法是不检索数组中的每一个hash值。更或者你可以只做一次比较就能确定字符串是否存在于数组的任何一个位置,听起来爽吧?
解决方案:hash表是一种特殊的数组,目标字符串的偏移量是目标字符串的hash值。根据应用程序需要创建指定大小的数组,(比如说1024,通常是2的n次方),想要确认新字符串是否在表里,为了获取新字符串在hash表的位置,将新字符串的hash按照hash表大小进行取模运算,余数就是新字符串在hash表的偏移量。然后将hash表指定偏移位置的字符串同新增字符串进行比较,如果不存在或者不相等,那么该字符串没有存在于hash表也即字符串数组中。代码如下:
int GetHashTablePos(char *lpszString, SOMESTRUCTURE *lpTable, int nTableSize) | ||
int nHash = HashString(lpszString), nHashPos = nHash % nTableSize; | ||
return nHashPos ; | ||
else | ||
return -1; //Error value | ||
} |
上面的方法有一个明显的问题,如果发生了冲突即两个不同的字符串hash后的值是一样的,又该怎么办?显然它们不能在hash表占用同样的位置。一个常用的解决方式是hash的每一个节点不再代表一个元素,而是一个列表的指针,该列表保存hash值为该偏移的所有字符串。
MPQs使用一个文件名称表来跟踪文件内部信息,但这个表和普通的hash表并不完全一样。首先:不是使用文件名称的hash值作为偏移量并存储真实的文件名来做校验;MPQs根本不会存储文件名称,而是使用三个不同的hash值:一个用来做hash表的偏移位置,两个用来做校验,两个用来校验的hash代替了真实的文件名称。这样还是有可能会出现两个不同的字符串hash到同样的三个值上,不过这样的概率非常小,大概是1:18889465931478580854784,谁识数的数一数,我是看不懂了,这样的概率对每一个人来说都是足够安全的。
另外MPQs和通常的hash实现不一样的地方是:没有为hash表的每一个入口保存一个链表,在冲突发生的时候继续向下找,直到找到一个未被占用的槽。
下面的代码描述了MPQs定位一个用来读的文件的过程
int GetHashTablePos(char *lpszString, MPQHASHTABLE *lpTable, int nTableSize) | |||
const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2; | |||
if (lpTable[nHashPos].nHashA == nHashA && lpTable[nHashPos].nHashB == nHashB) | |||
return nHashPos; | |||
else | |||
nHashPos = (nHashPos + 1) % nTableSize; | |||
if (nHashPos == nHashStart) | |||
break; | |||
} | |||
} |
下面是对代码的解释,大致符合程序查找并读取文件的过程。
1.计算三个hash值并保存在变量里。
2.移动到hash偏移量的入口。
3.该入口是否未使用,如果是,停止搜索,返回查找失败。
4.是否入口的两个检验hash等于计算出来的两个检验hash,如果是,停止搜索并返回该入口。
5.移动到链表的下一个入口,如果到了末尾就重新从开始查找。
6.如果查找的偏移量和计算的hash偏移量是一样的,已经搜索了整个表仍然没有找到,返回未查找到。
7.从第三步重新开始。
如果留心的话,可能会注意到该表是不可扩展的,如果所有的入口都被占据,那就不能够插入任何的文件名称,是的,这个表就是这样设计的,装载因子最大为1.0.甚至该表不可以动态扩大,因为扩大会导致所有的hash入口失效,并且不能重新生成该表,因为不知道相应的文件名称。
-----如果只是为了一个传奇的hash,这里就应该够了,hash的原理、hash的应用都有足够的说明。
压缩
问题:如果你有一个巨大的程序,比如50m,想要通过网络传输,但这是一个非常巨大的下载量,人们也可能没有足够的耐心来等待它下载完毕。
解决方案:压缩。压缩是将大量数据存储在较小空间的技术,差不多有上百种压缩算法,工作方式各不相同。MPQs使用的算法是数据压缩lib的方式,太过复杂不做介绍,会另开文章介绍。
本文的后半部分和主题关系不大,分篇章翻译,over。