hash相关

1)打造最快的Hash表(和Blizzard的对话)

先提一个简单的问题,如果有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?

有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但...也只能如此了。

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

Blizzard的这个算法是非常高效的,被称为"One-Way Hash",举个例子,字符串"unitneutralacritter.grp"通过这个算法得到的结果是0xA26067F3。
是不是把第一个算法改进一下,改成逐个比较字符串的Hash值就可以了呢,答案是,远远不够,要想得到最快的算法,就不能进行逐个的比较,通常是构造一个哈希表(Hash Table)来解决问题,哈希表是一个大数组,这个数组的容量根据程序的要求来定义,例如1024,每一个Hash值通过取模运算 (mod)对应到数组中的一个位置,这样,只要比较这个字符串的哈希值对应的位置又没有被占用,就可以得到最后的结果了,想想这是什么速度?是的,是最快的O(1),现在仔细看看这个算法吧
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
}

看到此,我想大家都在想一个很严重的问题:"如果两个字符串在哈希表中对应的位置相同怎么办?",毕竟一个数组容量是有限的,这种可能性很大。解决该问题的方法很多,我首先想到的就是用"链表",感谢大学里学的数据结构教会了这个百试百灵的法宝,我遇到的很多算法都可以转化成链表来解决,只要在哈希表的每个入口挂一个链表,保存所有对应的字符串就OK了。

事情到此似乎有了完美的结局,如果是把问题独自交给我解决,此时我可能就要开始定义数据结构然后写代码了。然而Blizzard的程序员使用的方法则是更精妙的方法。基本原理就是:他们在哈希表中不是用一个哈希值而是用三个哈希值来校验字符串。

中国有句古话"再一再二不能再三再四",看来Blizzard也深得此话的精髓,如果说两个不同的字符串经过一个哈希算法得到的入口点一致有可能,但用三个不同的哈希算法算出的入口点都一致,那几乎可以肯定是不可能的事了,这个几率是1:18889465931478580854784,大概是10的 22.3次方分之一,对一个游戏程序来说足够安全了。

现在再回到数据结构上,Blizzard使用的哈希表没有使用链表,而采用"顺延"的方式来解决问题,看看这个算法:
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, 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. 计算出字符串的三个哈希值(一个用来确定位置,另外两个用来校验)
2. 察看哈希表中的这个位置
3. 哈希表中这个位置为空吗?如果为空,则肯定该字符串不存在,返回
4. 如果存在,则检查其他两个哈希值是否也匹配,如果匹配,则表示找到了该字符串,返回
5. 移到下一个位置,如果已经越界,则表示没有找到,返回
6. 看看是不是又回到了原来的位置,如果是,则返回没找到
7. 回到3

怎么样,很简单的算法吧,但确实是天才的idea, 其实最优秀的算法往往是简单有效的算法,
Blizzard被称为最卓越的游戏制作公司,不愧于此。



2)了解 MD5 哈希
MD5 哈希用于通过网络发送加密的用户凭据(放在 HTTP 头中)。MD5 哈希(也称为“MD5 消息摘要”)是由与 HTTP 1.1 兼容的浏览器(如 Internet Explorer 5 和更高版本)使用 MD5 消息摘要算法创建的,有关该算法的定义,请参阅万维网联合会网站中的 RFC 1321 规范。

 注意 MD5 哈希比使用 Base64 编码的明文密码的安全性要高,因为如果使用 Base64 编码的密码被网络探测器截获,则未经授权的用户可以很容易地对该密码进行解码并重新使用。很难从哈希中解密使用 MD5 消息摘要算法加密的用户名和密码。

MD5 哈希包含用户名、密码和领域的名称。领域是对用户凭据进行验证或拒绝的域。用户凭据就是在 MD5 哈希中加密的密码。

有关在 IIS 服务器上设置领域名的信息,请参阅摘要式身份验证或高级摘要式身份验证。

MD5 哈希属性

MD5 哈希由少量的二进制数据(通常不超过 160 位)构成,并且是放在 HTTP 头中通过网络发送的。所有哈希值共享以下属性:

哈希长度 哈希值的长度取决于使用的运算法则类型,它的长度不随消息的大小而改变。最常见的哈希值长度为 128 或 160 位。
不可发现性 每一对不同的消息将转换为完全不同的哈希值,即使两个消息仅有一位不同也是如此。当今的技术还不可能发现转换为相同哈希值的一对消息。
可重复性 每次使用相同的算法对一个特别的消息进行哈希处理时,都将生成完全相同的哈希值。
不可逆性 所有的哈希算法都是单向的。即使给出哈希值,也无法发现密码。事实上,只给出哈希值并不能确定原始消息的任何属性。



3)MD5算法说明
来源:本文出自: (2001-06-25 08:10:00)

 
     1、MD5算法是对输入的数据进行补位,使得如果数据位长度LEN对512求余的结果是448。即数据扩展至K*512+448位。即K*64+56个字节,K为整数。具体补位操作:补一个1,然后补0至满足上述要求

     2、补数据长度:用一个64位的数字表示数据的原始长度B,把B用两个32位数表示。这时,数据就被填补成长度为512位的倍数。

     3. 初始化MD5参数:四个32位整数 (A,B,C,D) 用来计算信息摘要,初始化使用的是十六进制表示的数字

A=0X01234567 
B=0X89abcdef 
C=0Xfedcba98 
D=0X76543210 
4、处理位操作函数 
X,Y,Z为32位整数。 
F(X,Y,Z) = X&Y|NOT(X)&Z 
G(X,Y,Z) = X&Z|Y?(Z) 
H(X,Y,Z) = X xor Y xor Z 
I(X,Y,Z) = Y xor (X|not(Z)) 
     5、主要变换过程:使用常数组T[1 ... 64], T[i]为32位整数用16进制表示,数据用16个32位的整数数组M[]表示。

具体过程如下: 
/* 处理数据原文 */ 
For i = 0 to N/16-1 do 
/*每一次,把数据原文存放在16个元素的数组X中. */ 
For j = 0 to 15 do 
Set X[j] to M[i*16+j]. 
end /结束对J的循环 
/* Save A as AA, B as BB, C as CC, and D as DD. */ 
AA = A 
BB = B 
CC = C 
DD = D 
/* 第1轮*/ 
/* 以 [abcd k s i]表示如下操作 
a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */ 
/* Do the following 16 operations. */ 
[ABCD 0 7 1] [DABC 1 12 2] [CDAB 2 17 3] [BCDA 3 22 4] 
[ABCD 4 7 5] [DABC 5 12 6] [CDAB 6 17 7] [BCDA 7 22 8] 
[ABCD 8 7 9] [DABC 9 12 10] [CDAB 10 17 11] [BCDA 11 22 12] 
[ABCD 12 7 13] [DABC 13 12 14] [CDAB 14 17 15] [BCDA 15 22 16] 
/* 第2轮* */ 
/* 以 [abcd k s i]表示如下操作 
a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */ 
/* Do the following 16 operations. */ 
[ABCD 1 5 17] [DABC 6 9 18] [CDAB 11 14 19] [BCDA 0 20 20] 
[ABCD 5 5 21] [DABC 10 9 22] [CDAB 15 14 23] [BCDA 4 20 24] 
[ABCD 9 5 25] [DABC 14 9 26] [CDAB 3 14 27] [BCDA 8 20 28] 
[ABCD 13 5 29] [DABC 2 9 30] [CDAB 7 14 31] [BCDA 12 20 32] 
/* 第3轮*/ 
/* 以 [abcd k s i]表示如下操作 
a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */ 
/* Do the following 16 operations. */ 
[ABCD 5 4 33] [DABC 8 11 34] [CDAB 11 16 35] [BCDA 14 23 36] 
[ABCD 1 4 37] [DABC 4 11 38] [CDAB 7 16 39] [BCDA 10 23 40] 
[ABCD 13 4 41] [DABC 0 11 42] [CDAB 3 16 43] [BCDA 6 23 44] 
[ABCD 9 4 45] [DABC 12 11 46] [CDAB 15 16 47] [BCDA 2 23 48] 
/* 第4轮*/ 
/* 以 [abcd k s i]表示如下操作 
a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */ 
/* Do the following 16 operations. */ 
[ABCD 0 6 49] [DABC 7 10 50] [CDAB 14 15 51] [BCDA 5 21 52] 
[ABCD 12 6 53] [DABC 3 10 54] [CDAB 10 15 55] [BCDA 1 21 56] 
[ABCD 8 6 57] [DABC 15 10 58] [CDAB 6 15 59] [BCDA 13 21 60] 
[ABCD 4 6 61] [DABC 11 10 62] [CDAB 2 15 63] [BCDA 9 21 64] 
/* 然后进行如下操作 */ 
A = A + AA 
B = B + BB 
C = C + CC 
D = D + DD 
end /* 结束对I的循环*/ 
6、输出结果。 
 


4)用C#实现的数据加密(二) —— 哈希算法

     以下是用C#实现的哈希加密,大家可以根据自己的需要更改所需的算法,文中以SHA1为例:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace DataCrypto
{
 /// <summary>
 /// 哈希加密类
 /// </summary>
 public class HashMethod
 {
 
  private HashAlgorithm HashCryptoService;
  /// <summary>
  /// 哈希加密类的构造函数
  /// </summary>
  public HashMethod()
  {
   HashCryptoService = new SHA1Managed();
  }
  /// <summary>
  /// 加密方法
  /// </summary>
  /// <param name="Source">待加密的串</param>
  /// <returns>经过加密的串</returns>
  public string Encrypto(string Source)
  {
   byte[] bytIn = UTF8Encoding.UTF8.GetBytes(Source);
   byte[] bytOut = HashCryptoService.ComputeHash(bytIn);
   return Convert.ToBase64String(bytOut);
  }
 }
}


5)哈希表应用
 
 
2005-9-28 9:33:28 作者: 来源: 浏览:
 
一位中学生的论文

[摘要]

哈希表是一种高效的数据结构。本文分五个部分:首先提出了哈希表的优点,其次介绍了它的基础操作,接着从简单的例子中作了效率对比,指出其适用范围以及特点,然后通过例子说明了如何在题目中运用哈希表以及需要注意的问题,最后总结全文。

[正文]

引言
哈希表(Hash Table)的应用近两年才在NOI中出现,作为一种高效的数据结构,它正在竞赛中发挥着越来越重要的作用。

哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。

哈希表又叫做散列表,分为“开散列” 和“闭散列”。考虑到竞赛时多数人通常避免使用动态存储结构,本文中的“哈希表”仅指“闭散列”,关于其他方面读者可参阅其他书籍。

基础操作
基本原理
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方。

但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。后面我们将看到一种解决“冲突”的简便做法。

总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。


函数构造
构造函数的常用方法(下面为了叙述简洁,设 h(k) 表示关键字为 k 的元素所对应的函数值):


除余法:
选择一个适当的正整数 p ,令 h(k ) = k mod p

这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。

数字选择法:
如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组成的新的值作为关键字或者直接作为函数值。


冲突处理
线性重新散列技术易于实现且可以较好的达到目的。令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。


支持运算
哈希表支持的运算主要有:初始化(makenull)、哈希函数值的运算(h(x))、插入元素(insert)、查找元素(member)。

设插入的元素的关键字为 x ,A 为存储的数组。

初始化比较容易,例如

const empty=maxlongint; // 用非常大的整数代表这个位置没有存储元素

p=9997; // 表的大小

procedure makenull;

var i:integer;

begin

for i:=0 to p-1 do

A[i]:=empty;

End;


哈希函数值的运算根据函数的不同而变化,例如除余法的一个例子:

function h(x:longint):Integer;

begin

h:= x mod p;

end;


我们注意到,插入和查找首先都需要对这个元素定位,即如果这个元素若存在,它应该存储在什么位置,因此加入一个定位的函数 locate

function locate(x:longint):integer;

var orig,i:integer;

begin

orig:=h(x);

i:=0;

while (i<S)and(A[(orig+i)mod S]<>x)and(A[(orig+i)mod S]<>empty) do

inc(i);

//当这个循环停下来时,要么找到一个空的存储单元,要么找到这个元

//素存储的单元,要么表已经满了

locate:=(orig+i) mod S;

end;

插入元素

procedure insert(x:longint);

var posi:integer;

begin

posi:=locate(x); //定位函数的返回值

if A[posi]=empty then A[posi]:=x

else error; //error 即为发生了错误,当然这是可以避免的

end;


查找元素是否已经在表中

procedure member(x:longint):boolean;

var posi:integer;

begin

posi:=locate(x);

if A[posi]=x then member:=true

else member:=false;

end;


这些就是建立在哈希表上的常用基本运算。


下文提到的所有程序都能在附录中找到。

效率对比
3.1简单的例子与实验

下面是一个比较简单的例子:

===================================================================

集合 ( Subset )

问题描述:

给定两个集合A、B,集合内的任一元素x满足1 ≤ x ≤ 109,并且每个集合的元素个数不大于104 个。我们希望求出A、B之间的关系。只需确定在B 中但是不在 A 中的元素的个数即可。



这个题目是根据 OIBH NOIP 2002 模拟赛 # 1 的第一题改编的。


分析:我们先不管A 与 B 的具体关系如何,注意到这个问题的本质就是对于给定的集合A ,确定B 中的元素是否在 A 中。所以,我们使用哈希表来处理。至于哈希函数,只要按照除余法就行了,由于故意扩大了原题的数据规模, H(x) = x mod 15889;

当然本题可以利用别的方法解决,所以选取了速度最快的快速排序+二分查找,让这两种方法作效率对比。

我们假定 |A|=|B| ,对于随机生成的数据,计算程序重复运行50次所用时间。

对比表格如下:
哈希表(sec)
快速排序+二分查找(sec)

复杂度
一. O(N) (只有忽略了冲突才是这个结果。当然实际情况会比这个大,但是重复的几率与哈希函数有关,不容易估计)
二. O(N log N+ N) = O(N log N)

测试数据规模 一 二
500 0.957 0.578
1000 1.101 0.825
2500 1.476 1.565
5000 2.145 2.820
7500 2.905 4.203
10000 3.740 5.579
13500 7.775 7.753
15000 27.550 8.673

对于数据的说明:在 Celeron566 下用 TP 测试,为了使时间的差距明显,让程序重复运了行50次。同时哈希表中的P= 15889 ,下标范围 0..15888 。由于快速排序不稳定,因此使用了随机数据。


3.2 对试验结果的分析:

注意到两个程序的用时并不像我们期望的那样,总是哈希表快。设哈希表的大小为 P .


首先,当规模比较小的时候(大约为a< 10% * P,这个数据仅仅是通过若干数据估记出来的,没有严格证明,下同),第二种方法比哈希表快。这是由于,虽然每次计算哈希函数用O(1) 的时间,但是这个系数比较大。例如这道题的 H(x)=x mod 15589 ,通过与做同样次数的加法相比较,测试发现系数 > 12 ,因为 mod 运算本身与快速排序的比较大小和交换元素运算相比,比较费时间。所以规模小的时候,O(N)(忽略冲突)的算法反而不如 O(NlogN)。这一点在更复杂的哈希函数上会体现的更明显,因为更复杂的函数系数会更大。

其次,当规模稍大 (大约为 15%*P < a < 85%*P) 的时候,很明显哈希表的效率高。这是因为冲突的次数较少。

再次,当规模再大 (大约为 90%*P < a < P )的时候,哈希表的效率大幅下降。这是因为冲突的次数大大提高了,为了解决冲突,程序不得不遍历一段都存储了元素的数组空间来寻找空位置。用白箱测试的方法统计,当规模为13500的时候,为了找空位置,线性重新散列平均做了150000 次运算;而当规模为15000 的时候,平均竟然高达2000000 次运算,某些数据甚至能达到4265833次。显然浪费这么多次运算来解决冲突是不合算的,解决这个问题可以扩大表的规模,或者使用“开散列”(尽管它是动态数据结构)。然而需要指出的是,冲突是不可避免的。


初步结论:

当数据规模接近哈希表上界或者下界的时候,哈希表完全不能够体现高效的特点,甚至还不如一般算法。但是如果规模在中央,它高效的特点可以充分体现。我们可以从图像直观的观察到这一点。

试验表明当元素充满哈希表的 90% 的时候,效率就已经开始明显下降。这就给了我们提示:如果确定使用哈希表,应该尽量使数组开大(由于竞赛中可利用内存越来越多,大数组通常不是问题,当然也有少数情况例外),但对最太大的数组进行操作也比较费时间,需要找到一个平衡点。通常使它的容量至少是题目最大需求的 120% ,效果比较好(这个仅仅是经验,没有严格证明)。

应用举例
4.1 应用的简单原则

什么时候适合应用哈希表呢?如果发现解决这个问题时经常要询问:“某个元素是否在已知集合中?”,也就是需要高效的数据存储和查找,则使用哈希表是最好不过的了!那么,在应用哈希表的过程中,值得注意的是什么呢?

哈希函数的设计很重要。一个不好的哈希函数,就是指造成很多冲突的情况,从前面的例子已经可以看出来,解决冲突会浪费掉大量时间,因此我们的目标就是尽力避免冲突。前面提到,在使用“除余法”的时候,h(k)=k mod p ,p 最好是一个大素数。这就是为了尽力避免冲突。为什么呢?假设 p=1000 ,则哈希函数分类的标准实际上就变成了按照末三位数分类,这样最多1000类,冲突会很多。一般地说,如果 p 的约数越多,那么冲突的几率就越大。

简单的证明:假设 p 是一个有较多约数的数,同时在数据中存在 q 满足 gcd(p,q)=d >1 ,即有 p=a*d , q=b*d, 则有 q mod p= q – p* [q div p] =q – p*[b div a] . ① 其中 [b div a ] 的取值范围是不会超过 [0,b] 的正整数。也就是说, [b div a] 的值只有 b+1 种可能,而 p 是一个预先确定的数。因此 ① 式的值就只有 b+1 种可能了。这样,虽然mod 运算之后的余数仍然在 [0,p-1] 内,但是它的取值仅限于 ① 可能取到的那些值。也就是说余数的分布变得不均匀了。容易看出, p 的约数越多,发生这种余数分布不均匀的情况就越频繁,冲突的几率越高。而素数的约数是最少的,因此我们选用大素数。记住“素数是我们的得力助手”。

另一方面,一味的追求低冲突率也不好。理论上,是可以设计出一个几乎完美,几乎没有冲突的函数的。然而,这样做显然不值得,因为这样的函数设计很浪费时间而且编码一定很复杂,与其花费这么大的精力去设计函数,还不如用一个虽然冲突多一些但是编码简单的函数。因此,函数还需要易于编码,即易于实现。

综上所述,设计一个好的哈希函数是很关键的。而“好”的标准,就是较低的冲突率和易于实现。

另外,使用哈希表并不是记住了前面的基本操作就能以不变应万变的。有的时候,需要按照题目的要求对哈希表的结构作一些改进。往往一些简单的改进就可以带来巨大的方便。

这些只是一般原则,真正遇到试题的时候实际情况千变万化,需要具体问题具体分析才行。下面,我们看几个例子,看看这些原则是如何体现的。


有关字符串的例子
我们经常会遇到处理字符串的问题,下面我们来看这个例子:

======================================================================

找名字

问题描述:

给定一个全部由字符串组成的字典,字符串全部由大写字母构成。其中为每个字符串编写密码,编写的方式是对于 n 位字符串,给定一个 n 位数,大写字母与数字的对应方式按照电话键盘的方式:
2: A,B,C 5: J,K,L 8: T,U,V
3: D,E,F 6: M,N,O 9: W,X,Y
4: G,H,I 7: P,R,S
题目给出一个 1——12 位的数,找出在字典中出现且密码是这个数的所有字符串。字典中字符串的个数不超过 8000 。

这个是 USACO Training Gate 1.2.4 的一道题。

分析:看懂题目之后,对于给定的编码,只需要一个回溯的过程,所有可能的原字符串都可以被列举出来,剩下的就是检查这个字符串是否在给定的字典中了。所以这个问题需要的还是“某个元素是否在已知集合中?”由于给出的“姓名”都是字符串,因此我们可以利用字符的 ASCII 码。那么,如何设计这个哈希函数呢?注意到题目给出的字典中,最多能有5000 个不同元素,而一个字符的 ASCII 码只能有26 种不同的取值,因此至少需要用在3个位置上的字符(26^3 > 5000,但是 26^2 < 5000 ),于是我们就选取3个位置上的字符。由于给定的字符串的长度从 1——12 都有可能,为了容易实现,选取最开始的1个字符,和最末尾的2个字符。让这3个字符组成27进制的3位数,则这个数的值就是这个字符串的编码。这样哈希函数就设计出来了!
不过,由于可能出现只有1位的字符串,在写函数代码的时候需要特殊考虑;大素数选取 13883 。
这个函数是这样的:
function hash(s:string):integer;
var i,tmp:longint;
begin
tmp:=0; {用来记录27进制数的值}
if length(s)>1 then begin
tmp:=tmp*27+ord(s[1])-64;
for i:=1 downto 0 do
tmp:=tmp*27+ord(s[length(s)-i])-64; {取第一位和后两位}
end
else for i:=1 to 3 do
tmp:=tmp*27+ord(s[1])-64;{当长度为1的时候特殊处理}
hash:=tmp mod 13883;
end;

值得指出的是,本题给出的字符串大都没有什么规律,用哈希表可以做到近似“平均”,但是对于大多数情况,字符串是有规律的(例如英文单词),这个时候用哈希表反而不好(例如英语中有很多以 con 开头的单词),通常用检索树解决这样的查找问题。

在广度优先搜索中应用的例子
在广度优先搜索中,一个通用而且有效的剪枝就是在拓展节点之前先判重。而判重的本质也是数据的存储与查找,因此哈希表大有用武之地。来看下面的例子:

转花盆
题意描述:
    给定两个正6边形的花坛,要求求出从第一个变化到第二个的最小操作次数以及操作方式。一次操作是:选定不在边上的一盆花,将这盆花周围的6盆花按照顺时针或者逆时针的顺序依次移动一个单位。限定一个花坛里摆放的不同种类的花不超过3种,对于任意两种花,数量多的花的盆数至少是数量少的花的2倍
这是 SGOI-8 的一道题
分析:首先确定本题可以用广度优先搜索处理,然后来看问题的规模。正6边形共有19个格子可以用来放花,而且根据最后一句限定条件,至多只能存在 C(2,19) * C(5,17) = 1058148 种状态,用搜索完全可行。然而操作的时候,可以预料产生的重复节点是相当多的,需要迅速判重才能在限定时间内出解,因此想到了哈希表。那么这个哈希函数如何设计呢?注意到19个格子组成6边形是有顺序的,而且每一个格子只有3种可能情况,那么用3进制19位数最大 3^20-1=3486784400 用 Cardinal 完全可以承受。于是我们将每一个状态与一个整数对应起来,使用除余法就可以了。

小结
从这两个例子可以发现,对于字符串的查找,哈希表虽然不是最好的方法,但是每个字符都有“天生”的 ASCII 码,在设计哈希函数的时候可以直接利用。而其他方法,例如利用检索树的查找,编写代码不如哈希表简洁。至于广度优先搜索中的判重更是直接利用了哈希表的特点。

另外,我们看到这两个题目都是设计好哈希函数之后,直接利用前面的基本操作就可以了,因此重点应该是在哈希函数的设计上(尽管这两个例子的设计都很简单),需要注意题目本身可以利用的条件,以及估计值域的范围。下面我们看两个需要在哈希表基础上作一些变化的例子。

需要微小变化的例子
下面,我们来分析一道 NOI 的试题:

=======================================================================

方程的解数

问题描述
已知一个n元高次方程:
其中:x1, x2, …,xn是未知数,k1,k2,…,kn是系数,p1,p2,…pn是指数。且方程中的所有数均为整数。
假设未知数1≤ xi ≤M, i=1,,,n,求这个方程的整数解的个数。
约束条件
1£n£6;1£M£150;
方程的整数解的个数小于231。
本题中,指数Pi(i=1,2,……,n)均为正整数。
这个是 NOI 2001 的第二试中的《方程的解数》。
分析:初看此题,题目要求出给定的方程解的个数,这个方程在最坏的情况下可以有6个未知数,而且次数由输入决定。这样就不能利用数学方法直接求出解的个数,而且注意到解的范围最多150个数,因此恐怕只能使用枚举法了。最简单的思路是穷举所有未知数的取值,这样时间复杂度是 O(M^6) ,无法承受。因此我们需要寻找更好的方法,自然想到能否缩小枚举的范围呢?但是发现这样也有很大的困难。我们再次注意到M 的范围,若想不超时,似乎算法的复杂度上限应该是 O(M^3) 左右,这是因为 150^3 < 10000000 。这就启示我们能否仅仅通过枚举3个未知数的值来找到答案呢?如果这样,前一半式子的值 S 可以确定,这时只要枚举后3 个数的值,检查他们的和是否等于 -S 即可。这样只相当于在 O(M^3) 前面加了一个系数,当然还需要预先算出 1 到 150 的各个幂次的值。想到了这里,问题就是如何迅速的找到某个 S 是否曾经出现过,以及出现过了多少次,于是又变成了“某个元素是否在给定集合中”这个问题。所以,我们还是使用哈希表解决这个问题。至于哈希函数不是问题,还是把 S 的值作为关键字使用除余法即可。然而有一点需要注意,这个例子我们不仅需要纪录某个 S 是否出现,出现的次数也很重要,所以可以用一个2维数组,仅仅是加了一个存储出现次数的域而已。
Var
e:array[0..max-1,1..2]of longint; {e[x,1] 记录哈希函数值为 x 的 S 值, e[x,2] 记录这个 S 值出现了几次}
因此 insert 过程也需要一些变化:
procedure ins(x:longint);
var posi:longint;
begin
posi:=locate(x);
e[posi,1]:=x;
inc(e[posi,2]); {仅仅这一条语句,就可以记录下来 S 出现了几次}
end;

最后一个例子
下面我们来仔细分析下面这个问题:

迷宫的墙
题意描述:
神话中byte山边有一个井之迷宫。迷宫的入口在山顶。迷宫中有许多房间,每个的颜色是以下之一:红、绿、蓝。两个相同颜色的房间看起来相似而不可区分。每个房间里有三口井标以1,2,3。从一个房间到另一间只有一种方式:从上面房间的井里跳到(不一定竖直地)井底的房间。可以从入口房间到达任何其他房间。迷宫中的所有通路走向坐落在最底部的龙宫。所有的迷宫之旅对应了一系列在相继访问的房间里选择的井的标号。这一列数称为一个旅行计划。一个走过好几次迷宫的英雄bytezar画好了图,然而有的房间重复出现了多次。
输入:
第一行有一个整数n,2<=n<=6000,房间数(包括龙宫)。房间从1到n标号,较大编号的房间再较低处(入口房间编号1,龙宫编号n)。接下的n-1行描述迷宫的房间(除了龙宫)和井。每行有一个字母,一个空格,和三个由空格分隔的整数。字母代表了房间的颜色(C——红,Z——绿,N——蓝),第i(i=1,2,3)个数是第i个井通往的房间号。
输出:
迷宫最少的房间数目

这是 IOI 2003 中国国家集训队难题讨论活动的 0020 题。
分析:题目的意思是给出这个迷宫的地图,去掉重复出现的房间,找出这个迷宫的最少房间数目。于是关键就是确定什么样的房间是重复的。通过对样例的分析,可以看出这样的房间是重复的:如果两个房间 i 和 j (1<=i,j <=n-1),他们的颜色相同,而且第 k (k=1,2,3) 堵墙通向的房间或者相同、或者重复。因为这样从 i 和 j 可到达的房间是完全相同的。
所以,我们只需要记录下每个房间的情况和已经被确定相同的房间,然后挨个比较即可。于是又需要用到高效的数据存储与查找,自然想到哈希表。然而,这里面需要对哈希表作更大的改进:首先每个房间只能是3种颜色之一,因此针对每种颜色分别建立哈希表,可以使哈希函数的自变量减少一个;其次还需要纪录每个不重复的房间每堵墙都通向哪个房间,还有哪些房间是重复的。
具体这样实现:
var
e:array[0..2,0..p-1,1..4]of longint; {0..2 代表共有3种颜色,0..p-1 是哈希函数的值域,而 1..4 中的 1..3 表示三堵墙连接到那个房间,4 表示这个单元存储的是哪个节点}
r:array[1..maxn]of longint; {r[i] 表示与 i 相同的节点。如果有多个节点都是相同的,择取其中最大的(这一点不需要特殊的操作,只要在处理节点的时候注意就行了)}
至于哈希函数,最开始我是随意的写了一个(因为越是随意的,就越是随机的!),定位函数是这样的:
function locate(var a,b,c,d:longint):longint;
var t:longint;
i:integer;
begin
t:=r[b]*10037+r[c]*5953+r[d]*2999; {用3堵墙的值任意乘大素数相加再取余数,使得结果分布比较随机,也就比较均匀}
t:=t mod p;
i:=0;
while (e[a,(t+i)mod p,1]<>0)and(e[a,(t+i)mod p,1]<>r[b]) do
if (e[a,(t+i)mod p,2]<>r[c])or(e[a,(t+i)mod p,3]<>r[d]) then inc(i); {线性重新散列}
locate:=(t+i)mod p;
end;

但是后来发现完全没有必要这样做,这样的哈希函数在计算 t 的时候浪费了很多时间(不过数据规模不是很大,所以这点不十分明显),而且素数起到的作用也不应当是这样的。其实让 r[b],r[c],r[d] 组成 n 进制数就完全能够达到目的了,加入了素数不仅是小规模数据计算浪费时间,对大数据最后结果的分布平均也没有起到比 n 进制数更多的作用。因此改为
t:=r[b]*sqr(n)+r[c]*n+r[d];
当然肯定会有更好的哈希函数的。

小结
第一个例子,乍一看与哈希表毫无关系;第二个例子叙述比较复杂,但是经过仔细分析,发现问题的本质都是确定“某个元素是否在给定集合中”,这正是哈希表的特点。所以,不论题目的表面看起来如何,只要本质是需要高效的数据检索,哈希表通常就是最好的选择!
另外,这两个例子都在原来哈希表的基础上作了一些变化。第一个例子加入了纪录某个值出现次数的域,第二个例子加入了纪录所有墙的情况以及原节点编号的域。虽然都只是很小的变化,但是却给问题的解决带来了不小的方便。因此我们得到提示:哈希表虽然有标准的操作,但也不是一成不变的,需要具体问题具体分析,根据题目的要求和特点作出相应变化。
总结
本文介绍了有关哈希表方面的内容,分析了它的特点和优点,指出了应用需要注意的问题,并且重点举了几个例子来说明它在竞赛中的应用。希望读者读完本文能够对哈希表有更全面的了解,并能在竞赛中应用自如!

参考文献:

《算法与数据结构(第二版)》 付清祥 王晓东 编著
《奥赛兵法信息学(计算机)》 朱全民 主编
《SGOI-8 烦恼的设计师 解题报告》 曙光网信息学
《Data Structures》 USACO Training Gate

附录:
这是我第一次写论文,水平很有限,希望大家指出我的缺点和不足!
我的邮箱 iliuchong@sohu.com

下面是所有前面提到的程序。其中只有 SGOI-8 Flowers 的程序是网上提供的标程,其余的都是我自己写的,并且已经通过所有测试数据。

哈希表的程序
program subset;
const max=15889;
var fin,fout:text;
a,b,s,j:longint;
index:array[0..max-1]of longint;
t:real;
function locate(t:longint):longint;
var tmp:longint;
begin
tmp:=t mod max;
while (index[tmp]<>0)and(index[tmp]<>t) do
tmp:=(tmp+1) mod max;
locate:=tmp;
end;
procedure int(t:longint);
begin
index[locate(t)]:=t;
end;
function member(t:longint):boolean;
begin
if index[locate(t)]=t then member:=true
else member:=false;
end;
procedure init;
var shu,i:longint;
begin
assign(fin,'subset.in');
assign(fout,'subset.out');
reset(fin);
rewrite(fout);
close(fout);
fillchar(index,sizeof(index),0);
read(fin,a);
for i:=1 to a do
begin
read(fin,shu);
int(shu);
end;
end;
procedure main;
var i,shu:longint;
begin
read(fin,b);
s:=0;
for i:=1 to b do
begin
read(fin,shu);
if not member(shu) then inc(s);
end;
end;
procedure out;
begin
writeln(s);
close(fin);
end;
begin
t:=meml[$40:$6C];
for j:=1 to 50 do
begin
init;
main;
out;
end;
t:=meml[$40:$6C]-t;
writeln(t/18.181818:0:8);
end.

快速排序+二分查找的程序
program subset;
const max=16101;
var a,b,s,j:longint;
da:array[1..max]of longint;
fin:text;
t:real;
procedure init;
var i:longint;
begin
assign(fin,'subset.in');
reset(fin);
read(fin,a);
for i:=1 to a do
read(fin,da[i]);
end;
procedure sort(m,n:longint);
var p:longint;
function locate:longint;
var value,i,j,temp:longint;
begin
value:=da[(m+n) div 2];
i:=m-1;
j:=n+1;
while true do
begin
repeat
inc(i);
until da[i]>=value;
repeat
dec(j);
until da[j]<=value;
if i<j then begin
temp:=da[i];
da[i]:=da[j];
da[j]:=temp;
end
else begin
if I<>j then locate:=j
else locate:=j-1;;
exit;
end;
end;
end;
begin
if m<n then begin
p:=locate;
sort(m,p);
sort(p+1,n);
end;
end;
procedure main;
var i,x:longint;
function member(x:longint):boolean;
var p,e,mid:longint;
begin
p:=1;
e:=a;
mid:=(p+e) div 2;
while (p<>mid)and(e<>mid)and(da[mid]<>x) do
begin
if x=da[mid] then begin
member:=true;
exit;
end;
if x<da[mid] then begin
e:=mid;
mid:=(p+e)div 2;
end
else begin
p:=mid;
mid:=(p+e)div 2;
end;
end;
if (da[p]=x)or(da[e]=x)or(da[mid]=x) then member:=true
else member:=false;
end;
begin
read(fin,b);
s:=0;
for i:=1 to b do
begin
read(fin,x);
if not member(x) then inc(s);
end;
end;
procedure out;
begin
writeln(s);
close(fin);
end;
begin
t:=meml[$40:$6C];
for j:=1 to 50 do
begin
init;
sort(1,a);
main;
out;
end;
t:=meml[$40:$6C]-t;
writeln(t/18.181818:0:8);
end.

《找名字》的程序
program namenum;
const empty:string[12]=' ';
value:array[2..9,1..3]of string=(('A','B','C'),
('D','E','F'),
('G','H','I'),
('J','K','L'),
('M','N','O'),
('P','R','S'),
('T','U','V'),
('W','X','Y'));
var fin,fout,dict:text;
index:array[-1..13882]of string[12];

quest:string;
check:boolean;
function hash(s:string):integer;
var i,tmp:longint;
begin
tmp:=0;
if length(s)>1 then begin
tmp:=tmp*27+ord(s[1])-64;
for i:=1 downto 0 do
tmp:=tmp*27+ord(s[length(s)-i])-64;
end
else for i:=1 to 3 do
tmp:=tmp*27+ord(s[1])-64;
hash:=tmp mod 13883;
end;
function locate(s:string):integer;
var tmp,i:integer;
begin
tmp:=hash(s);
i:=0;
while (index[(i+tmp)mod 13883]<>s)and(index[(i+tmp)mod 13883]<>empty) do
i:=(i+23)mod 13883;
locate:=(i+tmp)mod 13883;
end;
procedure int(s:string);
var tmp:integer;
begin
tmp:=locate(s);
index[tmp]:=s;
end;
procedure init;
var s:string;
i:integer;
begin
assign(fin,{'d:/namenum.txt'}'namenum.in');
assign(fout,{'d:/namenum.out'}'namenum.out');
reset(fin);
rewrite(fout);
assign(dict,{'d:/dict1.txt'}'dict.txt');
reset(dict);
for i:=0 to 13882 do
index[i]:=empty;
while not eof(dict) do
begin
readln(dict,s);
int(s);
end;
close(dict);
readln(fin,quest);
close(fin);
end;
function member(s:string):boolean;
var tmp:integer;
begin
tmp:=locate(s);
if index[tmp]=s then member:=true
else member:=false;
end;
procedure work;
var st:string;
j:integer;
procedure examin(t:integer;ch:string);
var i:integer;
begin
if t=length(quest) then begin
st:=st+ch;
if member(st) then begin
writeln(fout,st);
check:=true;
end;
exit;
end;
st:=st+ch;
for i:=1 to 3 do
begin
examin(t+1,value[ord(quest[t+1])-ord('0'),i]);
delete(st,length(st),1);
end;
end;
begin
check:=false;
for j:=1 to 3 do
begin
st:='';
examin(1,value[ord(quest[1])-ord('0'),j]);
end;
if not check then writeln(fout,'NONE');
close(fout);
end;
begin
init;
work;
end.

《转花盆》的程序 (这个程序是 SGOI-8 Flowers 的标准程序)
program flowers;
const
size=1058148;
base=262143;

circle:array[1..7,1..6] of longint
=((1,2,6,10,9,4),
(2,3,7,11,10,5),
(4,5,10,14,13,8),
(5,6,11,15,14,9),
(6,7,12,16,15,10),
(9,10,15,18,17,13),
(10,11,16,19,18,14));
x:array[1..7] of longint=(2,2,3,3,3,4,4);
y:array[1..7] of longint=(2,3,2,3,4,2,3);
InputFn='flowers.in';
OutputFn='flowers.out';
var
last,next,q:array[1..size] of longint;
id:array[1..size] of shortint;
hash:array[0..base] of longint;
step,i,j,k,start,target,qs,l,r:longint;
bit,s,t:array[1..19] of longint;
d:array[0..7] of longint;
nowlast,nowid:longint;
f,fo:text;
procedure init;
var
d:array[0..5] of longint;
i,j:longint;
begin
assign(f,InputFn);
reset(f);
for i:=1 to 19 do
read(f,s[i]);
for i:=1 to 19 do
read(f,t[i]);
close(f);
d[0]:=0;
for i:=1 to 19 do
begin
inc(d[0]); d[d[0]]:=s[i];
for j:=1 to d[0] do
if d[j]=s[i] then break;
s[i]:=j-1;
if j<>d[0] then dec(d[0]);
inc(d[0]); d[d[0]]:=t[i];
for j:=1 to d[0] do
if d[j]=t[i] then break;
t[i]:=j-1;
if j<>d[0] then dec(d[0]);
end;
fillchar(next,sizeof(next),0);
fillchar(hash,sizeof(hash),0);
end;

function change(a,b:longint; plus:longint):longint;
var
i:longint;
begin
for i:=1 to 6 do
d[i]:=(a div bit[circle[b,i]]) mod 3;
d[7]:=d[1]; d[0]:=d[6];
for i:=1 to 6 do
a:=a+(d[i+plus]-d[i])*bit[circle[b,i]];
change:=a;
end;

procedure out;
var
i,j,dep:longint;
stack:array[1..20] of longint;
begin
i:=qs; dep:=0;
while i<>1 do
begin
inc(dep);
stack[dep]:=id[i];
i:=last[i];
end;
for i:=dep downto 1 do
if stack[i]>0 then
writeln(fo,x[stack[i]],' ',y[stack[i]],' ',1)
else writeln(fo,x[-stack[i]],' ',y[-stack[i]],' ',0);
end;

procedure insert(now:longint);
var
i:longint;
begin
if now=target then
begin
assign(fo,OutputFn);
rewrite(fo);
writeln(fo,step);
inc(qs); q[qs]:=now;
last[qs]:=nowlast; id[qs]:=nowid;
out;
close(fo);
halt;
end;
i:=now and base;
if hash[i]=0 then
begin
inc(qs); q[qs]:=now;
last[qs]:=nowlast; id[qs]:=nowid;
hash[i]:=qs;
end
else
begin
i:=hash[i];
while next[i]<>0 do
begin
if q[i]=now then exit;
i:=next[i];
end;
if q[i]=now then exit;
inc(qs); q[qs]:=now;
next[i]:=qs;
last[qs]:=nowlast; id[qs]:=nowid;
end;
end;

begin
init;
bit[1]:=1;
for i:=2 to 19 do
bit[i]:=bit[i-1]*3;
start:=0; target:=0;
for i:=1 to 19 do
begin
start:=start+s[i]*bit[i];
target:=target+t[i]*bit[i];
end;
r:=0; qs:=0; step:=0;
insert(start);
repeat
l:=r+1; r:=qs;
inc(step);
for i:=l to r do
for j:=1 to 7 do
begin
k:=change(q[i],j,1);
nowlast:=i; nowid:=j;
insert(k);
k:=change(q[i],j,-1);
nowlast:=i; nowid:=-j;
insert(k);
end;
until qs=r;
end.

《方程的解》的程序
program equation;
const maxm=150;
max=4000037;
var k,p:array[1..6]of longint;
fin,fout:text;
n,m:integer;
ans:longint;
mi:array[1..maxm,1..30]of longint;
e:array[0..max-1,1..2]of longint;
procedure init;
var i:integer;
begin
assign(fin,'equation.in');
assign(fout,'equation.out');
reset(fin);
rewrite(fout);
read(fin,n);
read(fin,m);
for i:=1 to n do
read(fin,k[i],p[i]);
close(fin);
fillchar(e,sizeof(e),0);
ans:=0;
end;
procedure precompute;
var i,j:integer;
tmp:longint;
begin
for j:=1 to 30 do
mi[1,j]:=1;
for i:=2 to m do
begin
tmp:=1;
for j:=1 to trunc(ln(maxlongint)/ln(i)) do
begin
tmp:=tmp*i;
mi[i,j]:=tmp;
end;
end;
end;
function locate(x:longint):longint;
var i,t:longint;
begin
t:=abs(x) mod max;
i:=0;
while (e[(t+i)mod max,1]<>0)and(e[(t+i)mod max,1]<>x) do
inc(i);
locate:=(t+i) mod max;
end;
procedure ins(x:longint);
var posi:longint;
begin
posi:=locate(x);
e[posi,1]:=x;
inc(e[posi,2]);
end;
procedure solve(a:integer;s:longint);
var i:integer;
begin
if a=n div 2 then begin
for i:=1 to m do
ins(s+k[a]*mi[i,p[a]]);
exit;
end;
for i:=1 to m do
solve(a+1,s+k[a]*mi[i,p[a]]);
end;
procedure find(a:integer;s:longint);
var i:integer;
tmp,posi:longint;
begin
if a=n then begin
for i:=1 to m do
begin
tmp:=-1*(s+k[a]*mi[i,p[a]]);
posi:=locate(tmp);
if e[posi,1]=tmp then inc(ans,e[posi,2]);
end;
exit;
end;
for i:=1 to m do
find(a+1,s+k[a]*mi[i,p[a]]);
end;
procedure out;
begin
writeln(fout,ans);
close(fout);
end;
procedure special;
var i:integer;
begin
if n=1 then begin
if p[1]=0 then writeln(fout,m)
else writeln(fout,0);
close(fout);
halt;
end;
end;
begin
init;
precompute;
special;
solve(1,0);
find(n div 2+1,0);
out;
end.

《迷宫的墙》的程序
program the_100_tough_problems_0020_walls_in_the_maze;
const maxn=6001;
p=7499;
var fin,fout:text;
n,ans:integer;
data:array[1..maxn,1..4]of longint;
e:array[0..2,0..p-1,1..4]of longint;
r:array[1..maxn]of longint;
procedure init;
var i,j:integer;
ch:char;
begin
assign(fin,'d:/test/lab14.in');
assign(fout,'d:/test/lab.txt');
reset(fin);
rewrite(fout);
readln(fin,n);
for i:=1 to n-1 do
begin
read(fin,ch);
case ch of
'N':data[i,1]:=0;
'Z':data[i,1]:=1;
'C':data[i,1]:=2;
end;
for j:=2 to 4 do
read(fin,data[i,j]);
readln(fin);
end;
close(fin);
ans:=n;
fillchar(e,sizeof(e),0);
for i:=1 to n do
r[i]:=i;
end;
function locate(var a,b,c,d:longint):longint;
var t:longint;
i:integer;
begin

t:=r[b]*sqr(n)+r[c]*n+r[d];
t:=t mod p;
i:=0;
while (e[a,(t+i)mod p,1]<>0)and(e[a,(t+i)mod p,1]<>r[b]) do
if (e[a,(t+i)mod p,2]<>r[c])or(e[a,(t+i)mod p,3]<>r[d]) then inc(i);
locate:=(t+i)mod p;
end;
procedure main;
var i,j,t,k:integer;
begin
for i:=n-1 downto 1 do
begin
t:=locate(data[i,1],data[i,2],data[i,3],data[i,4]);
if e[data[i,1],t,1]<>r[data[i,2]] then begin
for j:=2 to 4 do
e[data[i,1],t,j-1]:=r[data[i,j]];
e[data[i,1],t,4]:=i;
end
else begin
dec(ans);
r[i]:=e[data[i,1],t,4];
{for j:=1 to i-1 do
for k:=2 to 4 do
if data[j,k]=i then
data[j,k]:=e[data[i,1],t,4];}
end;
end;
end;
procedure out;
begin
writeln({fout,}ans);
close(fout);
end;
begin
init;
main;
out;
end.
 

6)一个开源的哈希表源码实现
作者:yyaadet      来源:     发表时间:2006-06-25     浏览次数: 4543      字号:大  中  小
First let's see what data struction he used.
struct entry
{
    void *k, *v;
    unsigned int h;
    struct entry *next;
};
struct hashtable {
    unsigned int tablelength;
    struct entry **table;
    unsigned int entrycount;
    unsigned int loadlimit;
    unsigned int primeindex;
    unsigned int (*hashfn) (void *k);
    int (*eqfn) (void *k1, void *k2);
};

static const unsigned int primes[] = {
53, 97, 193, 389,
769, 1543, 3079, 6151,
12289, 24593, 49157, 98317,
196613, 393241, 786433, 1572869,
3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189,
805306457, 1610612741
};
const unsigned int prime_table_length = sizeof(primes)/sizeof(primes[0]);
const float max_load_factor = 0.65;

The import function is creat_hashtable here.
struct hashtable *create_hashtable(unsigned int minsize,
                 unsigned int (*hashf) (void*),
                 int (*eqf) (void*,void*))
{
    struct hashtable *h;
    unsigned int pindex, size = primes[0];
    /* Check requested hashtable isn't too large */
    if (minsize > (1u << 30)) return NULL;
    /* Enforce size as prime */
    for (pindex=0; pindex < prime_table_length; pindex++) {
        if (primes[pindex] > minsize) { size = primes[pindex]; break; }
    }
    h = (struct hashtable *)malloc(sizeof(struct hashtable));
    if (NULL == h) return NULL; /*oom*/
    h->table = (struct entry **)malloc(sizeof(struct entry*) * size);
    if (NULL == h->table) { free(h); return NULL; } /*oom*/
    memset(h->table, 0, size * sizeof(struct entry *));
    h->tablelength  = size;
    h->primeindex   = pindex;
    h->entrycount   = 0;
    h->hashfn       = hashf;
    h->eqfn         = eqf;
    h->loadlimit    = (unsigned int) ceil(size * max_load_factor);
    return h;
}
We can find that "creat_hashtable" is initialing some data.
unsigned int hash(struct hashtable *h, void *k)
{
    /* Aim to protect against poor hash functions by adding logic here
     * - logic taken from java 1.4 hashtable source */
    unsigned int i = h->hashfn(k);
    i += ~(i << 9);
    i ^=  ((i >> 14) | (i << 18)); /* >>> */
    i +=  (i << 4);
    i ^=  ((i >> 10) | (i << 22)); /* >>> */
    return i;
}
I have not understood hash(struct hashtable,void *);




8)Gzip Zlib PNG 压缩算法,源码详解


发布者:hanfajie   加入时间:2006-2-24

内容:

  gzip,zlib,以及图形格式png,使用的是同一个压缩算法deflate。我们通过对gzip源码的分析来对deflate压缩算法做一个详细的说明。我阅读的gzip版本为 gzip-1.2.4。我们对算法做三种程度的说明。第一种程度,对gzip所使用压缩算法基本原理的说明。第二种程度,对gzip压缩算法实现方法的说明。第三种程度,对gzip实现源码级的说明。
  
  如果你有时间的话,我建议你先不要看下面的内容,自己尝试通过读gzip源码,来了解它的压缩解压缩是如何实现的,这将会是一个非常有趣的智力游戏,千万不要错过。当一个又一个的谜被解开时,那感觉就像唐伯虎同志所说的,“慷慨然诺杯酒中”。(小唐的诗,除了另一个倒霉蛋曹雪芹外,好像不太被人提。)
  
  1 gzip所使用压缩算法的基本原理
  
  gzip 对于要压缩的文件,首先使用lz77算法进行压缩,对得到的结果再使用huffman编码的方法进行压缩。所以我们分别对lz77和huffman编码的原理进行说明。
  
  1.1 ... 1.2 ...
  
  2 gzip压缩算法实现方法
  
  2.1 LZ77算法的gzip实现
  
  首先,gzip 从要压缩的文件中读入64KB的内容到一个叫window的缓冲区中。为了简单起见,我们以32KB以下文件的压缩为例做说明。对于我们这里使用32KB以下文件,gzip将整个文件读入到window缓冲区中。然后使用一个叫strstart的变量在window数组中,从0开始一直向后移动。strstart在每一个位置上,都在它之前的区域中,寻找和当前strstart开始的串的头3个字节匹配的串,并试图从这些匹配串中找到最长的匹配串。
  
  如果当前的strstart开始的串,可以找到最少为3个字节的匹配串的话,当前的strstart开始的匹配长度那么长的串,将会被一个<匹配长度,到匹配串开头的距离>对替换。
  
  如果当前的strstart开始的串,找不到任何的最少为3个字节的匹配串的话,那么当前strstart的所在字节将不作改动。
  
  为了区分是一个<匹配长度,到匹配串开头的距离>对,还是一个没有被改动的字节,还需要为每一个没有被改动的字节或者<匹配长度,到匹配串开头的距离>对,另外再占用一
  位,来进行区分。这位如果为1,表示是一个<匹配长度,到匹配串开头的距离>对,这位如果为0,表示是一个没有被改动的字节。
  
  现在来说明一下,为什么最小匹配为3个字节。这是由于,gzip 中,<匹配长度,到匹配串开头的距离>对中,"匹配长度"的范围为3-258,也就是256种可能值,需要8bit来保存。"到匹配串开头的距离"的范围为0-32K,需要15bit来保存。所以一个<匹配长度,到匹配串开头的距离>对需要23位,差一位3个字节。如果匹配串小于3个字节的话,使用<匹配长度,到匹配串开头的距离>对进行替换,不但没有压缩,反而还会增大。所以保存<匹配长度,到匹配串开头的距离>对所需要的位数,决定了最小匹配长度至少要为3个字节。
  
  下面我们就来介绍gzip如何实现寻找当前strstart开始的串的最长匹配串。
  
  如果每次为当前串寻找匹配串时,都要和之前的每个串的至少3个字节进行比较的话,那么比较量将是非常非常大的。为了提高比较速度,gzip使用了哈希表。这是gzip实现LZ77的关键。这个哈希表是一个叫head的数组(后面我们将看到为什么这个缓冲区叫head)。gzip对windows中的每个串,使用串的头三个字节,也就是strstart,strstart+1,strstart+2,用一个设计好的哈希函数来进行计算,得到一个插入位置ins_h。也就是用串的头三个字节来确定一个插入位置。然后把串的位置,也就是 strstart的值,保存在head数组的第ins_h项中。我们马上就可以看到为什么要这样做。head数组在没有插入任何值时,全部为0。

当某处的当前串的三个字节确定了一个ins_h,并把当时当前串的位置也就是当时的strstart保存在了head[ins_h]中。之后另一处,当另一处的当前串的头三个字节,再为那三个字节时,再使用那个哈希函数来计算,由于是同样的三个字节,同样的哈希函数,得到的ins_h必然和前面得到的ins_h是相同的。于是就会发现head[ins_h]不为0。这就说明了,有一个头三个字节和自己相同的串把自己的位置保存在了这里,现在head[ins_h]中保存的值,也就是那个串的开始位置,我们就可以找到那个串,那个串至少前3个字节和当前串的前3个字节相同(稍后我们就可以看到这种说法不准确,这里是为了说明方便),我们可以找到那个串,做进一步比较,看到底能有多长的匹配。
  
  我们现在来说明一下,相同的三个字节,通过哈希函数得到的ins_h必然是相同的。而不同的三个字节,通过哈希函数有没有可能得到同一个ins_h,我没有对这个哈希函数做研究,并不清楚,不过一般的哈希函数都是这样的,所以极大可能这里的也会是这种情况,即不同的三个字节,通过哈希函数有可能得到同一个ins_h,不过这并不要紧,我们发现有可能是匹配串之后,还会进行串的比较。
  
  一个文件中,可能有很多个串的头三个字节都是相同的,也就是说他们计算得到的ins_h都是相同的,如何能保证找到他们中的每一个串呢?gzip使用一个链把他们链在一起。gzip每次把当前串的位置插入head的当前串头三个字节算出的ins_h处时,都会首先把原来的head[ins_h]的值,保存到一个叫prev的数组中,保存的位置就在现在的strstart处。这样当以后某处的当前串计算出ins_h,发现head[ins_h]不空时,就可以到prev[ head[ins_h] ]中找到更前一个的头三个字节相同的串的位置。对此我们举例说明。
  
  例,串
  0abcdabceabcfabcg
  ^^^^^^^^^^^^^^^^^
  01234567890123456
  
  整个串被压缩程序处理之后。
  
  由abc算出ins_h。
  这时的head[ins_h]中为 13,即"abcg"的开始位置。
  这时prev[13]中为 9,即"abcfabcg"的开始位置。
  这时prev[9]中为 5,即"abceabcfabcg"的开始位置。
  这时prev[5]中为 1,即"abcdabceabcfabcg"的开始位置。
  这时prev[1]中为 0。
  
  我们看到所有头三个字母为abc的串,被链在了一起,从head可以一直找下去,直到找到0。
  
  现在我们也就知道了,三个字节通过哈希函数计算得到同一ins_h的所有的串被链在了一起,head[ins_h]为链头,prev数组中放着的更早的串。这也就是head和prev名称的由
  来。
  
  gzip寻找匹配串的另外一个值得注意的实现是,延迟匹配。会进行两次尝试。比如当前串为str,那么str发生匹配以后,并不发生压缩,还会对str+1串进行匹配,然后看哪种
  匹配效果好。
  
  例子 ...
从这个例子中我们就看到了做另外一次尝试的原因。如果碰到的一个匹配就使用了的话,可能错过更长匹配的机会。现在做两次会有所改善。
  
  ...
  
  2.2 问题讨论
  
  我在这里对gzip压缩算法做出了一些说明,是希望可以和对gzip或者压缩解压缩感兴趣的朋友进行交流。
  我对gzip的了解要比这里说的更多一些,也有更多的例子。如果哪位朋友愿意对下面的问题进行研究,以及其他压缩解压缩的问题进行研究,来这里http://jiurl.cosoft.org.cn/forum/ 和我交流的话,我也愿意就我知道的内容进行更多的说明。
  
  下面是几个问题
  
  这种匹配算法,即用3个字节(最小匹配)来计算一个整数,是否比用串比较来得高效,高效到什么程度。
  
  哈希函数的讨论。不同的三个字节,是否可能得到同一个ins_h。ins_h和计算它的三个字节的关系。
  
  几次延迟尝试比较好?
  
  用延迟,两次尝试是否对压缩率的改善是非常有限的?
  
  影响lz77压缩率的因素。
  
  压缩的极限。
    
  2.3 ...
  
  3 gzip源码分析
  
  main() 中调用函数 treat_file() 。
  treat_file() 中打开文件,调用函数 zip()。注意这里的 work 的用法,这是一个函数指针。
  zip() 中输出gzip文件格式的头,调用 bi_init,ct_init,lm_init,
  其中在lm_init中将 head 初始化清0。初始化strstart为0。从文件中读入64KB的内容到window缓冲区中。
  由于计算strstart=0时的ins_h,需要0,1,2这三个字节和哈希函数发生关系,所以在lm_init中,预读0,1两个字节,并和哈希函数发生关系。
  
  然后lm_init调用 deflate()。
  deflate() gzip的LZ77的实现主要deflate()中。
 


 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值