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. 散列表的装填因子。
散列表的装填因子定义为:α= 填入表中的元素个数 / 散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。
实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。
/***************************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) 请按任意键继续. . . */