原文来自: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;

while (*lpszString != 0)
{

ulHash <<= 1;
ulHash += *lpszString++;
}

return ulHash;

}

上面的代码提供了一种简单的hash算法,累加字符串的所有字符,在累加字符串之前hash的基准值左移一位。这个算法简单也很有用,但会生成大量相似的输出,并且经常是在较小的值域内产生大量冲突,冲突是指两个不同的字符串hash后值一样。

MPQ格式使用了非常复杂的hash算法来生成全部不可预期的值,事实上该hash算法是一个单向算法,单向算法是指无法通过hash值反算出原始字符串。下面是这个特别的算法:

unsigned long HashString(char *lpszFileName, unsigned long dwHashType)
{
unsigned char *key = (unsigned char *)lpszFileName;
unsigned long seed1 = 0x7FED7FED, 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;
}

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;

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

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;
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 + 1) % nTableSize;
 
if (nHashPos == nHashStart)
break;
}

return -1; //Error value

}

下面是对代码的解释,大致符合程序查找并读取文件的过程。

1.计算三个hash值并保存在变量里。

2.移动到hash偏移量的入口。

3.该入口是否未使用,如果是,停止搜索,返回查找失败。

4.是否入口的两个检验hash等于计算出来的两个检验hash,如果是,停止搜索并返回该入口。

5.移动到链表的下一个入口,如果到了末尾就重新从开始查找。

6.如果查找的偏移量和计算的hash偏移量是一样的,已经搜索了整个表仍然没有找到,返回未查找到。

7.从第三步重新开始。

如果留心的话,可能会注意到该表是不可扩展的,如果所有的入口都被占据,那就不能够插入任何的文件名称,是的,这个表就是这样设计的,装载因子最大为1.0.甚至该表不可以动态扩大,因为扩大会导致所有的hash入口失效,并且不能重新生成该表,因为不知道相应的文件名称。

-----如果只是为了一个传奇的hash,这里就应该够了,hash的原理、hash的应用都有足够的说明。

压缩

问题:如果你有一个巨大的程序,比如50m,想要通过网络传输,但这是一个非常巨大的下载量,人们也可能没有足够的耐心来等待它下载完毕。

解决方案:压缩。压缩是将大量数据存储在较小空间的技术,差不多有上百种压缩算法,工作方式各不相同。MPQs使用的算法是数据压缩lib的方式,太过复杂不做介绍,会另开文章介绍。

本文的后半部分和主题关系不大,分篇章翻译,over。

暴雪的hash算法[翻译]的更多相关文章

  1. Hash冲突的解决--暴雪的Hash算法

    Hash冲突的解决--暴雪的Hash算法https://usench.iteye.com/blog/2199399https://www.bbsmax.com/A/kPzOO7a8zx/

  2. 暴雪HASH算法(转)

    暴雪公司有个经典的字符串的hash公式 先提一个简单的问题,假如有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做? 有一个方法最简单,老老实实 ...

  3. hash算法总结收集

    hash算法的意义在于提供了一种快速存取数据的方法,它用一种算法建立键值与真实值之间的对应关系,(每一个真实值只能有一个键值,但是一个键值可以对应多个真实值),这样可以快速在数组等条件中里面存取数据. ...

  4. 【学】常用hash算法的介绍

    基本知识 Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值.这种转换是一种压缩映 ...

  5. hash算法

    作者:July.wuliming.pkuoliver 说明:本文分为三部分内容, 第一部分为一道百度面试题Top K算法的详解:第二部分为关于Hash表算法的详细阐述:第三部分为打造一个最快的Hash ...

  6. Hash算法初见

    hash算法 (hashmap 实现原理)   Hash ,一般翻译做“ 散列” ,也有直接音译为“ 哈希” 的,就是把任意长度的输入(又叫做预映射, pre-image ),通过散列算法,变换成固定 ...

  7. 【整理】hash算法原理及常见函数

    简介 Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入,通过散列算法,变换成固定长度的输出,该输出就是散列值.        散列表 ...

  8. [速成]了解一致性hash算法

    定义 一致性hash算法,在维基百科的定义是: Consistent hashing is a special kind of hashing such that when a hash table ...

  9. 【数据结构与算法】一致性Hash算法及Java实践

    追求极致才能突破极限 一.案例背景 1.1 系统简介 首先看一下系统架构,方便解释: 页面给用户展示的功能就是,可以查看任何一台机器的某些属性(以下简称系统信息). 消息流程是,页面发起请求查看指定机 ...

随机推荐

  1. 面试官问你:MYSQL事务和隔离级别,该如何回答

    一.事务 事务是由一组SQL语句组成的逻辑处理单元,是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚.事务具有以下4个属性,通常简称为事务 ...

  2. Day5 练习

    1. 2. 要加强对代码效率的思考 3. 4. 知识点:1)多变量的同时赋值 2)元素的不可修改性:(1,2)=(3,4)与(a,b)= (b,a)     联想指针.地址去理解  :可用id()或t ...

  3. sqlserver系统表使用

    SELECT s.table_catalog as 数据库名, o.name as 表名, c.name as 列名FROM INFORMATION_SCHEMA.TABLES s,--库 sys.o ...

  4. C# 常用语句

    var list = dt.AsEnumerable().Select(t => t.Field<string>("Bed")).ToList();Select( ...

  5. Linux基础:uniq命令总结

    本文只总结一些常用的用法,更详细的说明见man uniq和 uniq --help. uniq命令 uniq命令主要用于去重. 需要注意的是,不相邻的行不算重复值. 语法格式 Usage: uniq ...

  6. MySQL7.5.15数据库配置主从服务器实现双机热备实例教程

    环境说明 程序在:Web服务器192.168.0.57上面 数据库在:MySQL服务器192.168.0.67上面 实现目的:增加一台MySQL备份服务器(192.168.0.68),做为MySQL服 ...

  7. UOJ #131 BZOJ 4199 luogu P2178【NOI2015】品酒大会 (后缀自动机、树形DP)

    水是水,但是写出了不少问题,因此写一发博客. https://www.luogu.org/problemnew/show/P2178 https://www.lydsy.com/JudgeOnline ...

  8. 学习EXTJS6(5)基本功能-进度条组件

    Ext.ProgressBar有二种模式:手动和自动:手动:自己控制显示.进度.更新.清除.自动只需要调用Wait方法即可. 配置项:   配置项 类型 说明 renderTo String 指定页面 ...

  9. Future和Callable的使用

    应用场景 财务成本核算.可能会有多个耗时的步骤.如果顺序执行是非常慢的.再相互数据获取数据不依赖的情况下可以使用Future并行执行 public class FutureTest implement ...

  10. rmq算法,利用倍增思想

    RMQ问题ST算法  /*  RMQ(Range Minimum/Maximum Query)问题:      RMQ问题是求给定区间中的最值问题.当然,最简单的算法是O(n)的,但是对于查询次数很多 ...