【定义】:
1.哈希函数:在记录的存储位置和它的关键字之间建立一个确定的对应关系ƒ,使每个关键字和结构中的一个唯一的存储位置相对应,称这个对应关系ƒ为哈希函数(Hash Function)(或散列函数)。
2.冲突:对不同的关键字可能得到同一哈希地址,即key1≠key2,而ƒ(key1)=ƒ(key2)
,这种现象称冲突(collision)。
3.哈希表:根据设定的哈希函数H(key)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为哈希表(Hash Table)(或散列表),这一映射过程称为哈希造表或散列,所得的存储位置称哈希地址或散列地址。
【构造方法】:
常用的构造哈希函数的方法:
哈希函数能使对一个数据序列的访问过程更加迅速有效,通过哈希函数,数据元素将被更快地定位。
实际工作中需视不同的情况采用不同的哈希函数,通常考虑的因素有:
·计算哈希函数所需时间
·关键字的长度
·哈希表的大小
·关键字的分布情况
· 记录的查找频率
1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)。若其中H(key)中已经有值了,就往下一个找,直到H(key)中没有值了,就放进去。
2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法:取关键字平方后的中间几位作为散列地址。
4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
5. 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
处理冲突的方法:
1. 开放寻址法:Hi=(H(key) +di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
1.1. di=1,2,3,…,m-1,称线性探测再散列;
1.2. di=1^2,-1^2,2^2,-2^2,⑶^2,…,±(k)^2,(k<=m/2)称二次探测再散列;
1.3. di=伪随机数序列,称伪随机探测再散列。
2. 再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
3. 链地址法(拉链法)
4. 建立一个公共溢出区
【查找的性能分析】
散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
1. 散列函数是否均匀;
2. 处理冲突的方法;
3. 散列表的装填因子。
散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。
实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。
【C代码实现】
/***************************ElemType.h***************************/
#ifndef ELEMTYPE_H//头文件保护符
#define ELEMTYPE_H
#define NULL_KEY 0//0为无记录标志
typedef int KeyType;//关键字类型
typedef struct ElemType
{//数据元素(记录)类型
KeyType key;
int order;
}ElemType;
//对两个数值型关键字的比较约定为如下的宏定义。
#define EQ(a,b) ((a) == (b))
#define LT(a,b) ((a) < (b))
#define LE(a,b) ((a) <= (b))
//基本操作函数声明
void Visit(int p,ElemType r);
#endif
/***************************ElemType.cpp***************************/
#include "ElemType.h"
#include <stdio.h>
void Visit(int p,ElemType r)
{
printf("address = %d(%d,%d)\n",p,r.key,r.order);
}
/***************************HashTable.h***************************/
/*本算法中采用的构造哈希表的方法有:
1.构造哈希函数的方法:【除留余数法】
H(key) = key MOD p (p ≤ m),其中m为表长,本算法中取p=m;(一般情况下,可选p为质数或不包含小于20的质因数的合数)
2.处理冲突的方法:【开放定址法】
Hi = (H(key) + di) MOD m, i=1,2,3,...,k(k <= m - 1),其中di为增量序列,可有下列三种取法:
(1) 线性探索再散列:di = 1,2,3,...,m-1
(2) 二次探索再散列:di = 1,-1^2,2,-2^2,...,±k^2(k <= m - 1)
(3) 伪随机探索再散列:di = 伪随机数序列
*/
#ifndef HASHTABLE_H//头文件保护符
#define HASHTABLE_H
#include "ElemType.h"
static int hashsize[] = {11,19,29,37};//内部链接静态变量,哈希表容量(表长m)递增表,一个合适的素数序列。
typedef struct HashTable
{//哈希表的存储结构
ElemType *elem;//记录存储基址变量,动态分配数组
int count;//当前记录个数
int sizeindex;//hashsize[sizeindex]为当前表长
}HashTable;
//宏定义
typedef int Status; // Status是函数的类型,其值是函数结果状态代码,如下边三个宏定义
#define SUCCESS 1 //查找成功
#define UNSUCCESS 0//查找失败
#define DUPLICATE -1//记录重复
//基本操作函数声明
void InitHashTable(HashTable &H);
void DestroyHashTable(HashTable &H);
unsigned Hash(KeyType k);
int d(int i);
void collision(KeyType &k,int &p,int i);
Status SearchHash(HashTable H,KeyType k,int &p,int &c);
Status InsertHash(HashTable &H,ElemType e);
void RecreateHashTable(HashTable &H);
void TraverseHash(HashTable H,void (*Visit)(int,ElemType));
#endif
/***************************HashTable.cpp***************************/
#include <stdio.h>//NULL等
#include <stdlib.h>//exit()、 rand()
#include <malloc.h>//malloc
#include <math.h>//OVERFLOW、pow()
#include "HashTable.h"
static int m;//内部链接静态变量,m为哈希表表长
void InitHashTable(HashTable &H)
{//初始化哈希表,构造一个记录为空的哈希表
int i;
H.sizeindex = 0;//初始化表长数组索引为0
m = hashsize[H.sizeindex];//初始化表长为hashsize[0]
H.count = 0;//初始化记录数为0
H.elem = (ElemType *)malloc(m * sizeof(ElemType));//动态分配记录数组
if (!H.elem)
{//分配失败
exit(OVERFLOW);
}
for (i = 0; i < m; ++i)
{//初始化记录数组关键字为空
H.elem[i].key = NULL_KEY;//未填记录的标志
}
}
void DestroyHashTable(HashTable &H)
{//销毁哈希表
free(H.elem);//释放动态记录数组
H.elem = NULL;//指针置为空
H.count = 0;//记录数置为为0
H.sizeindex = 0;//表长索引项置为0
}
unsigned Hash(KeyType k)
{//返回哈希函数求得的哈希地址(用除留余数法构造的一个简单的哈希函数)
return k % m;// H(key) = key MOD p (p ≤ m),取p=m
}
int d(int i)
{//增量序列函数,其中i为冲突次数。根据需要在3种方法选取一种,其他两种注释掉
return i;//线性探索再散列:di = 1,2,3,...,m-1
// return ((i + 1)/2) * ((i + 1)/2) * (int)pow(double(-1),i - 1);//二次探索再散列:di = 1,-1^2,2,-2^2,...,±k^2(k <= m - 1)
// return rand();//伪随机探索再散列:di = 伪随机数序列
}
void collision(KeyType &k,int &p,int i)
{//用开放定址法处理冲突,其中p为处理后所得的哈希地址,i为冲突次数。
p = (Hash(k) + d(i)) % m;
}
Status SearchHash(HashTable H,KeyType k,int &p,int &c)
{//在开放地址哈希表中查找关键字为K的记录,若查找成功,以p指示记录在表中的位置,并返回SUCCESS;
//否则以p指示插入位置,并返回UNSUCCESS。c以计冲突次数,其初值为0,供建表插入时参考
p = Hash(k);//用哈希函数计算哈希地址
while (H.elem[p].key != NULL_KEY && !EQ(H.elem[p].key,k))
{//该位置中填有记录,并且与待查记录不相等
c++;
if (c < m)
{//处理冲突次数未超出m - 1,则继续处理冲突
collision(k,p,c);
}
else
{//超出最大处理次数,H中位找到记录
break;
}
}
if (EQ(H.elem[p].key,k))
{//查找成功
return SUCCESS;
}
else
{//查找失败
return UNSUCCESS;
}
}
void RecreateHashTable(HashTable &);//对函数RecreateHashTable()的声明
Status InsertHash(HashTable &H,ElemType e)
{
int p,c = 0;//冲突次数初始为0
if (SearchHash(H,e.key,p,c))
{//查找成功
return DUPLICATE;//H中已有与e有相同关键字的记录,不插入
}
else if (c < hashsize[H.sizeindex]/2)
{//未找到,冲突次数c也未达到上限(c的阀值可调,但最大不超过hashsize[H.sizeindex] - 1)
H.elem[p] = e;//在H中插入数据元素e
++H.count;
return SUCCESS;//插入成功
}
else
{//未找到,但冲突次数c已达到上限
RecreateHashTable(H);//重建哈希表
return UNSUCCESS;//插入不成功
}
}
void RecreateHashTable(HashTable &H)
{
int i,count = H.count;//H中原有记录个数
ElemType *p,*elem = (ElemType *)malloc(count *sizeof(ElemType));//动态生成存放哈希表H原有数据的空间
p =elem;
for (i = 0; i < m; ++i)
{//将原有的所有记录,保存到elem中
if (!EQ(H.elem[i].key,NULL_KEY))
{//H在该单元有记录
*p++ = H.elem[i];//将记录依次存入elem
}
}
H.count = 0;//将原有记录数置为0,为下面调用InserHash做准备
H.sizeindex++;//表长数组索引加1
m = hashsize[H.sizeindex];//新的存储容量(表长)
H.elem = (ElemType *)realloc(H.elem,m*sizeof(ElemType));//以新的存储容量重新生成空哈希表H
for (i = 0; i < m; ++i)
{//初始化新的哈希表
H.elem[i].key = NULL_KEY;//未填记录
}
for (p = elem; p < elem + count; ++p)
{//将原表中的记录重新存储到新的哈希表中
InsertHash(H,*p);
}
free(elem);//释放elem的存储空间
}
void TraverseHash(HashTable H,void (*Visit)(int,ElemType))
{//按哈希地址的顺序遍历哈希表H
int i;
printf("哈希地址 0 ~ %d\n",m - 1);
for (i = 0; i < m; ++i)
{//对于整个哈希表H
if (!EQ(H.elem[i].key,NULL_KEY))
{//H在第i个单元有记录
Visit(i,H.elem[i]);//访问第i个数据
}
}
}
Records.txt中的内容
17 1
60 2
29 3
38 4
1 5
2 6
3 7
4 8
60 9
13 10
/***************************main.cpp***************************/
#include <stdio.h>
#include "HashTable.h"
#define N 15//数组可容纳的记录个数
int main(void)
{
ElemType r[N];//记录数组
HashTable h;
int i,n = 0,p = 0;
Status s;
KeyType k;
FILE *f;//文件指针类型
f = fopen("Records.txt","r");//打开记录文件Record.txt
do
{
i = fscanf(f,"%d%d",&r[p].key,&r[p].order);//先将记录暂存入记录数组r[p]
if (i != -1)
{//输入数据成功
p++;
}
} while (!feof(f) && p < N);//未到达数据文件结尾且记录数组未满
fclose(f);//关闭数据文件
InitHashTable(h);
for (i = 0; i < p - 1; ++i)
{//在h中插入前p-1个记录(最后一个记录不插入,插入最后一个记录会重建哈希表)
s = InsertHash(h,r[i]);
if (DUPLICATE == s)
{
printf("表中已有关键字为%d的记录,无法再插入记录(%d,%d)\n",r[i].key,r[i].key,r[i].order);
}
}
printf("按哈希地址的顺序遍历哈希表:\n");
TraverseHash(h,Visit);
printf("请输入待查找记录的关键字:");
scanf("%d",&k);
s = SearchHash(h,k,p,n);
if (SUCCESS == s)
{//查找成功
Visit(p,h.elem[p]);//输出该记录
}
else
{//查找失败
printf("未找到\n");
}
s = InsertHash(h,r[i]);//插入最后一个记录(需重建哈希表)
if (UNSUCCESS == s)
{//插入不成功
s = InsertHash(h,r[i]);//重建哈希表后重新插入
}
printf("按哈希表地址的顺序遍历重建后的哈希表:\n");
TraverseHash(h,Visit);
printf("请输入待查找记录的关键字:");
scanf("%d",&k);
s = SearchHash(h,k,p,n);
if (SUCCESS == s)
{//查找成功
Visit(p,h.elem[p]);//输出该记录
}
else
{//查找失败
printf("未找到\n");
}
DestroyHashTable(h);
return 0;
}
/*******************************运行结果*******************************/
/*
表中已有关键字为60的记录,无法再插入记录(60,9)
按哈希地址的顺序遍历哈希表:
哈希地址 0 ~ 10
address = 1(1,5)
address = 2(2,6)
address = 3(3,7)
address = 4(4,8)
address = 5(60,2)
address = 6(17,1)
address = 7(29,3)
address = 8(38,4)
请输入待查找记录的关键字:13
未找到
按哈希表地址的顺序遍历重建后的哈希表:
哈希地址 0 ~ 18
address = 0(38,4)
address = 1(1,5)
address = 2(2,6)
address = 3(3,7)
address = 4(4,8)
address = 5(60,2)
address = 10(29,3)
address = 13(13,10)
address = 17(17,1)
请输入待查找记录的关键字:13
address = 13(13,10)
请按任意键继续. . .
*/