先提一个简单的问题,如果有一个庞大的字符串数组,然后给你一个单独的字符串,让你从这个数组中查找是否有这个字符串并找到它,你会怎么做?
有一个方法最简单,老老实实从头查到尾,一个一个比较,直到找到为止,我想只要学过程序设计的人都能把这样一个程序作出来,但要是有程序员把这样的程序交给用户,我只能用无语来评价,或许它真的能工作,但...也只能如此了。
最合适的算法自然是使用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"。
(转载注:所谓One-Way Hash,就是无法从求得的hash值通过简单的逆运算就得到原来的字符串。类似与加密算法中的message digest.)
是不是把第一个算法改进一下,改成逐个比较字符串的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)
{ //比较的是Table中存储的另外两个Hash函数的值,Table中不存储字符串
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被称为最卓越的游戏制作公司,不愧于此。(转载注:这种解决hash collision的方法相对于用linked list方法的缺点在于,hash表的entry只能代表一个字符串,如果hash表满了则无法在向hash表中加入新的entry)
近期由于需要,研究了魔兽文件打包管理器的相关算法,重点对其文件索引表的生成和查找进行了研究:采用哈希表进行,在冲突方面的处理方面,采用线性探测再散列。在添加和查找过程中进行了三次哈希,第一个哈希值用来查找,后两个哈希值用来校验,这样可以大大减少冲突的几率。
这里对其进行了简单的封装,扩展时,仅仅需要对结构体进行扩展即可。更为详细的说明,参考代码:【转载请保留版权,谢谢】
一、类声明头文件
view plaincopy to clipboardprint?
/
// Name: HashAlgo.h
// Purpose: 使用魔兽Hash算法,实现索引表的填充和查找功能。
// Author: 陈相礼
// Modified by:
// Created: 07/30/09
// RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $
// Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved.
// Licence:
/
#define MAXFILENAME 255 // 最大文件名长度
#define MAXTABLELEN 1024 // 默认哈希索引表大小
//
// 测试宏定义,正式使用时关闭
#define DEBUGTEST 1
//
// 哈希索引表定义
typedef struct
{
long nHashA;
long nHashB;
bool bExists;
char test_filename[MAXFILENAME];
// ......
} MPQHASHTABLE;
//
// 对哈希索引表的算法进行封装
class CHashAlgo
{
public:
#if DEBUGTEST
long testid; // 测试之用
#endif
CHashAlgo( const long nTableLength = MAXTABLELEN )// 创建指定大小的哈希索引表,不带参数的构造函数创建默认大小的哈希索引表
{
prepareCryptTable();
m_tablelength = nTableLength;
m_HashIndexTable = new MPQHASHTABLE[nTableLength];
for ( int i = 0; i < nTableLength; i++ )
{
m_HashIndexTable[i].nHashA = -1;
m_HashIndexTable[i].nHashB = -1;
m_HashIndexTable[i].bExists = false;
m_HashIndexTable[i].test_filename[0] = '/0';
}
}
void prepareCryptTable(); // 对哈希索引表预处理
unsigned long HashString(char *lpszFileName, unsigned long dwHashType); // 求取哈希值
long GetHashTablePos( char *lpszString ); // 得到在定长表中的位置
bool SetHashTable( char *lpszString ); // 将字符串散列到哈希表中
unsigned long GetTableLength(void);
void SetTableLength( const unsigned long nLength );
~CHashAlgo()
{
if ( NULL != m_HashIndexTable )
{
delete []m_HashIndexTable;
m_HashIndexTable = NULL;
m_tablelength = 0;
}
}
protected:
private:
unsigned long cryptTable[0x500];
unsigned long m_tablelength; // 哈希索引表长度
MPQHASHTABLE *m_HashIndexTable;
};
/
// Name: HashAlgo.h
// Purpose: 使用魔兽Hash算法,实现索引表的填充和查找功能。
// Author: 陈相礼
// Modified by:
// Created: 07/30/09
// RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $
// Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved.
// Licence:
/
#define MAXFILENAME 255 // 最大文件名长度
#define MAXTABLELEN 1024 // 默认哈希索引表大小
//
// 测试宏定义,正式使用时关闭
#define DEBUGTEST 1
//
// 哈希索引表定义
typedef struct
{
long nHashA;
long nHashB;
bool bExists;
char test_filename[MAXFILENAME];
// ......
} MPQHASHTABLE;
//
// 对哈希索引表的算法进行封装
class CHashAlgo
{
public:
#if DEBUGTEST
long testid; // 测试之用
#endif
CHashAlgo( const long nTableLength = MAXTABLELEN )// 创建指定大小的哈希索引表,不带参数的构造函数创建默认大小的哈希索引表
{
prepareCryptTable();
m_tablelength = nTableLength;
m_HashIndexTable = new MPQHASHTABLE[nTableLength];
for ( int i = 0; i < nTableLength; i++ )
{
m_HashIndexTable[i].nHashA = -1;
m_HashIndexTable[i].nHashB = -1;
m_HashIndexTable[i].bExists = false;
m_HashIndexTable[i].test_filename[0] = '/0';
}
}
void prepareCryptTable(); // 对哈希索引表预处理
unsigned long HashString(char *lpszFileName, unsigned long dwHashType); // 求取哈希值
long GetHashTablePos( char *lpszString ); // 得到在定长表中的位置
bool SetHashTable( char *lpszString ); // 将字符串散列到哈希表中
unsigned long GetTableLength(void);
void SetTableLength( const unsigned long nLength );
~CHashAlgo()
{
if ( NULL != m_HashIndexTable )
{
delete []m_HashIndexTable;
m_HashIndexTable = NULL;
m_tablelength = 0;
}
}
protected:
private:
unsigned long cryptTable[0x500];
unsigned long m_tablelength; // 哈希索引表长度
MPQHASHTABLE *m_HashIndexTable;
};
二、类实现文件
view plaincopy to clipboardprint?
/
// Name: HashAlgo.cpp
// Purpose: 使用魔兽Hash算法,实现索引表的填充和查找功能。
// Author: 陈相礼
// Modified by:
// Created: 07/30/09
// RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $
// Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved.
// Licence:
/
#include "windows.h"
#include "HashAlgo.h"
//
// 预处理
void CHashAlgo::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 );
}
}
}
//
// 求取哈希值
unsigned long CHashAlgo::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;
}
//
// 得到在定长表中的位置
long CHashAlgo::GetHashTablePos(char *lpszString)
{
const unsigned long HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;
unsigned long nHash = HashString(lpszString, HASH_OFFSET);
unsigned long nHashA = HashString(lpszString, HASH_A);
unsigned long nHashB = HashString(lpszString, HASH_B);
unsigned long nHashStart = nHash % m_tablelength,
nHashPos = nHashStart;
while ( m_HashIndexTable[nHashPos].bExists)
{
if (m_HashIndexTable[nHashPos].nHashA == nHashA && m_HashIndexTable[nHashPos].nHashB == nHashB)
return nHashPos;
else
nHashPos = (nHashPos + 1) % m_tablelength;
if (nHashPos == nHashStart)
break;
}
return -1; //没有找到
}
//
// 通过传入字符串,将相应的表项散列到索引表相应位置中去
bool CHashAlgo::SetHashTable( char *lpszString )
{
const unsigned long HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;
unsigned long nHash = HashString(lpszString, HASH_OFFSET);
unsigned long nHashA = HashString(lpszString, HASH_A);
unsigned long nHashB = HashString(lpszString, HASH_B);
unsigned long nHashStart = nHash % m_tablelength,
nHashPos = nHashStart;
while ( m_HashIndexTable[nHashPos].bExists)
{
nHashPos = (nHashPos + 1) % m_tablelength;
if (nHashPos == nHashStart)
{
#if DEBUGTEST
testid = -1;
#endif
return false;
}
}
m_HashIndexTable[nHashPos].bExists = true;
m_HashIndexTable[nHashPos].nHashA = nHashA;
m_HashIndexTable[nHashPos].nHashB = nHashB;
strcpy( m_HashIndexTable[nHashPos].test_filename, lpszString );
#if DEBUGTEST
testid = nHashPos;
#endif
return true;
}
//
// 取得哈希索引表长
unsigned long CHashAlgo::GetTableLength(void)
{
return m_tablelength;
}
//
// 设置哈希索引表长
void CHashAlgo::SetTableLength( const unsigned long nLength )
{
m_tablelength = nLength;
return;
}
/
// Name: HashAlgo.cpp
// Purpose: 使用魔兽Hash算法,实现索引表的填充和查找功能。
// Author: 陈相礼
// Modified by:
// Created: 07/30/09
// RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $
// Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved.
// Licence:
/
#include "windows.h"
#include "HashAlgo.h"
//
// 预处理
void CHashAlgo::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 );
}
}
}
//
// 求取哈希值
unsigned long CHashAlgo::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;
}
//
// 得到在定长表中的位置
long CHashAlgo::GetHashTablePos(char *lpszString)
{
const unsigned long HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;
unsigned long nHash = HashString(lpszString, HASH_OFFSET);
unsigned long nHashA = HashString(lpszString, HASH_A);
unsigned long nHashB = HashString(lpszString, HASH_B);
unsigned long nHashStart = nHash % m_tablelength,
nHashPos = nHashStart;
while ( m_HashIndexTable[nHashPos].bExists)
{
if (m_HashIndexTable[nHashPos].nHashA == nHashA && m_HashIndexTable[nHashPos].nHashB == nHashB)
return nHashPos;
else
nHashPos = (nHashPos + 1) % m_tablelength;
if (nHashPos == nHashStart)
break;
}
return -1; //没有找到
}
//
// 通过传入字符串,将相应的表项散列到索引表相应位置中去
bool CHashAlgo::SetHashTable( char *lpszString )
{
const unsigned long HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;
unsigned long nHash = HashString(lpszString, HASH_OFFSET);
unsigned long nHashA = HashString(lpszString, HASH_A);
unsigned long nHashB = HashString(lpszString, HASH_B);
unsigned long nHashStart = nHash % m_tablelength,
nHashPos = nHashStart;
while ( m_HashIndexTable[nHashPos].bExists)
{
nHashPos = (nHashPos + 1) % m_tablelength;
if (nHashPos == nHashStart)
{
#if DEBUGTEST
testid = -1;
#endif
return false;
}
}
m_HashIndexTable[nHashPos].bExists = true;
m_HashIndexTable[nHashPos].nHashA = nHashA;
m_HashIndexTable[nHashPos].nHashB = nHashB;
strcpy( m_HashIndexTable[nHashPos].test_filename, lpszString );
#if DEBUGTEST
testid = nHashPos;
#endif
return true;
}
//
// 取得哈希索引表长
unsigned long CHashAlgo::GetTableLength(void)
{
return m_tablelength;
}
//
// 设置哈希索引表长
void CHashAlgo::SetTableLength( const unsigned long nLength )
{
m_tablelength = nLength;
return;
}
三、测试主文件
view plaincopy to clipboardprint?
/
// Name: DebugMain.cpp
// Purpose: 测试Hash算法封装的类,完成索引表的填充和查找功能的测试。
// Author: 陈相礼
// Modified by:
// Created: 07/30/09
// RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $
// Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved.
// Licence:
/
//
// 测试参数设定宏
#define TESTNUM 32
#include <iostream>
#include <fstream>
#include "HashAlgo.h"
using namespace std;
//
// 测试主函数开始
int main( int argc, char **argv )
{
CHashAlgo hash_test( TESTNUM );
cout << "取得初始化散列索引表长为:" << hash_test.GetTableLength() << endl;
bool is_success = hash_test.SetHashTable( "test" );
if ( is_success )
{
cout << "散列结果一:成功!" << endl;
}
else
{
cout << "散列结果一:失败!" << endl;
}
is_success = hash_test.SetHashTable( "测试" );
if ( is_success )
{
cout << "散列结果二:成功!" << endl;
}
else
{
cout << "散列结果二:失败!" << endl;
}
long pos = hash_test.GetHashTablePos( "test" );
cout << "查找测试字符串:/"test/" 的散列位置:" << pos << endl;
pos = hash_test.GetHashTablePos( "测试" );
cout << "查找测试字符串:“测试” 的散列位置:" << pos << endl;
//
// 散列测试
for ( int i = 0; i < TESTNUM; i++ )
{
char buff[32];
sprintf(buff, "abcdefg%d.", i);
is_success = hash_test.SetHashTable(buff);
is_success ? cout << buff << "散列结果:成功!位置:" << hash_test.testid << endl : cout << buff << "散列结果:失败!" << endl;
}
system( "pause" );
//
// 查找测试
for ( int i = 0; i < TESTNUM; i++ )
{
char buff[32];
sprintf(buff, "abcdefg%d.", i);
pos = hash_test.GetHashTablePos( buff );
pos != -1 ? cout << "查找测试字符串:"<< buff <<" 的散列位置:" << pos << endl : cout << buff << "存在冲突!" << endl;
}
system( "pause" );
return 0;
}
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/eaglewood2005/archive/2009/07/30/4394583.aspx