散
列表设计
(刘爱贵 - Aiguille.LIU)
1、基本概念
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
2、常用的构造散列函数的方法
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。散列表的常用构造方法有:
(1)直接定址法
(2)数字分析法
(3)平方取中法
(4)折叠法
(5)随机数法
(6)除留余数法
3、处理冲突的方法
散列表函数设计好的情况下,可以减少冲突,但是无法完全避免冲突。常见有冲突处理方法有:
(1)开放定址法
(2)再散列法
(3)链地址法(拉链法)
(4)建立一个公共溢出区
4、散列表查找性能分析
散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
1. 散列函数是否均匀;
2. 处理冲突的方法;
3. 散列表的装填因子。
散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度。
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。
(以上内容的详细介绍可以参见参考文献1。)
5、一个散列表实例
"The C Programming Language"一书中给出了一个散列表例子。它的实现代码很典型,可以在宏处理器或编译器的符号表管理例程中找到。完整的C代码如下:
#include
<
stdio.h
>
#include
<
string
.h
>

#define
HASHSIZE101


struct
nlist
...
{
structnlist*next;
char*keys;
char*value;
}
;

static
struct
nlist
*
hashtable[HASHSIZE];

unsignedhash(
char
*
s)

...
{
unsignedhashval;

for(hashval=0;*s!='';s++)
hashval=*s+31*hashval;
returnhashval%HASHSIZE;
}

struct
nlist
*
hashtable_search(
char
*
s)

...
{
structnlist*np;

for(np=hashtable[hash(s)];np!=NULL;np=np->next)
if(strcmp(s,np->keys)==0)
returnnp;
returnNULL;
}

struct
nlist
*
hashtable_insert(
char
*
keys,
char
*
value)

...
{
structnlist*np;
unsignedhashval;


if((np=hashtable_search(keys))==NULL)...{
np=(structnlist*)malloc(sizeof(*np));
if(np==NULL||(np->keys=strdup(keys))==NULL)
returnNULL;
hashval=hash(keys);
np->next=hashtable[hashval];
hashtable[hashval]=np;
}else
free((void*)np->value);
if((np->value=strdup(value))==NULL)
returnNULL;
returnnp;
}

char
*
hashtable_getvalue(
char
*
keys)

...
{
structnlist*np;

if((np=hashtable_search(keys))==NULL)
returnNULL;
else
returnnp->value;
}

int
main(
int
argc,
char
*
argv[])

...
{
char*ret;

hashtable_insert("INT_MAX","32767");
hashtable_insert("INT_MIN","-32768");
hashtable_insert("LONG_MAX","2147483647");
hashtable_insert("LONG_MIN","-2147483647");

if((ret=hashtable_getvalue(argv[1]))==NULL)
printf("%snotfound ",argv[1]);
else
printf("%s=%s ",argv[1],ret);
}
散列函数hash,它通过一个for循环进行计算,每次循环中,它将上一次循环中计算得到的结果经过变换(即乘以31)后得到的新值同字符中的当前字符的值相加(*s + 31 * hashval),然后将该结果值同数据长度执行取模操作,其结果即是该函数的返回值。这个散列函数并不是最好的,但比较简短有效。另外,上面代码中采用链地址法来处理冲突,对桶大小未作限制。
这个散列函数到底是否真的简短有效呢?我们使用C语言中的保留关键字对其进行分析和测试。C语言的保留关键字有32个(如下面代码中定义),散列表长度为101。因此装填因子
α = 32 /101 = 0.32
这个装填因子比较小,从理论上说冲突的可能性较小,但牺牲了较多的空间,以空间换取了效率。
我们使用如下的程序对C语言保留关键字进行hash计算:
#define
HASHSIZE101
unsignedhash(
char
*
s)

...
{
unsignedhashval;

for(hashval=0;*s!='';s++)
hashval=*s+31*hashval;
returnhashval%HASHSIZE;
}


char
*
keywords[]
=
...
{
"auto","break","case","char","const","continue","default","do",
"double","else","enum","extern","float","for","goto","if",
"int","long","register","return","short","signed","sizeof","static",
"struct","switch","typedef","union","unsigned","void","volatile","while"
}
;


int
main(
void
)
...
{
inti,size,pos;
intcount[HASHSIZE];

for(i=0;i<HASHSIZE;i++)
count[i]=0;

size=sizeof(keywords)/sizeof(keywords[0]);
for(i=0;i<size;i++)
count[hash(keywords[i])]++;


for(i=0;i<size;i++)...{
pos=hash(keywords[i]);
printf("%-10s:%-3d%d ",keywords[i],pos,count[pos]);
}
return0;
}
我们可以得到如下的输出结果:
auto:
10
1
break
:
0
1
case
:
32
1
char
:
53
1
const
:
14
1
continue
:
87
1
default
:
17
1
do
:
80
1
double
:
76
1
else
:
91
1
enum
:
63
1
extern
:
16
1
float
:
57
1
for
:
72
1
goto
:
78
1
if
:
24
1
int
:
98
1
long
:
66
1
register:
49
1
return
:
96
1
short
:
99
1
signed:
81
1
sizeof
:
51
1
static
:
85
1
struct
:
1
1
switch
:
5
1
typedef:
2
1
union:
22
1
unsigned:
4
1
void
:
70
1
volatile
:
71
1
while
:
100
1
从这个结果可以看出,散列表中的数据分布比较均匀,而且更为理想的是,居然没有发生一个冲突。可见,书中所说的“简短有效”确实名副其实,甚至是非常理想的散列函数。这使得我不禁又要推荐“C程序设计语言”一书啦!
6、参考文献
(1)严蔚敏,吴伟民. 数据结构(C语言版).清华大学出版社,1997年。
(2)Brian W. Kernighan, Dennis M. Ritchie. 徐宝文等译. C程序设计语言(第2版),机械工业出版社,2008年。