28、哈希表(Hash)的查找
一、哈希表相关概念
1、哈希函数的基本概念
哈希表又称散列表。
哈希表存储的基本思想是:以数据表中的每个记录的关键字 k为自变量,通过一种函数H(k)计算出函数值。把这个值解释为一块连续存储空间(即数组空间)的单元地址(即下标),将该记录存储到这个单元中。在此称该函数H为哈希函数或散列函数。按这种方法建立的表称为哈希表或散列表。
理想情况下,哈希函数在关键字和地址之间建立了一个一一对应关系,从而使得查找只需一次计算即可完成。由于关键字值的某种随机性,使得这种一一对应关系难以发现或构造。因而可能会出现不同的关键字对应一个存储地址。即k1≠k2,但H(k1)=H(k2),这种现象称为冲突。把这种具有不同关键字值而具有相同哈希地址的对象称“同义词”。
在大多数情况下,冲突是不能完全避免的。这是因为所有可能的关键字的集合可能比较大,而对应的地址数则可能比较少。
对于哈希技术,主要研究两个问题:
(1)如何设计哈希函数以使冲突尽可能少地发生。
(2)发生冲突后如何解决。
2、哈希函数的构造方法
常见的构造方法有很多种,如直接定址法,数字分析法,平方取中法等。接下来,我们介绍其中的几种:
(1)除留余数法
取关键字k被某个不大于表长m的数p除后所得余数作为哈希函数地址的方法。即:
H(k)=k mod p
这种方法的关键是选择好p。使得数据集合中的每一个关键字通过该函数转化后映射到哈希表的任意地址上的概率相等。理论研究表明,一般取p为小于m的最大质数或不包含小于20的质因素的合数。
(2)平方取中法
先将关键字平方,然后取其中间几位作为散列地址。所取位数由地址空间范围决定。若地址空间小于所取位数值决定的范围,可通过乘以一比例因子来解决。
(3)折叠法
把关键字分割成位数相等(最后一部分的位数可以不同)的几部分,然后通过折叠后将几部分进行相加,丢掉进位位,所得值即为散列地址。散列的位数由地址空间的位数而定。
分割方法:从右至左
相加方法有两种:
移位叠加:将分割后的各部分低位对齐相加。
界间叠加:将某些部分倒置后再相加。相当于把关键字看成一张纸,从一端向另一端沿间界逐层折叠,再把相应位数相加。
3、哈希函数的冲突检测方法
假设哈希表的地址范围为0~m-l,当对给定的关键字k,由哈希函数H(k)算出的哈希地址为i(0≤i≤m-1)的位置上已存有记录,这种情况就是冲突现象。
处理冲突就是为该关键字的记录找到另一个“空”的哈希地址。即通过一个新的哈希函数得到一个新的哈希地址。如果仍然发生冲突,则再求下一个,依次类推。直至新的哈希地址不再发生冲突为止。
常用的处理冲突的方法有开放地址法、链地址法等几类。
(1)开放地址法
当发生冲突时,将依次探测“下一个位置”,直到找到其关键字相匹配的元素或找到一个空位插入。设哈希空间长度为m,“下一个位置”由下式确定:
Hi=(H(key)+di) mod m
H(key):哈希函数
m:哈希表长度
di:求“下一个位置”的增量
di的确定方法
a) 线性探测再散列
di=1,2,…,m-1。
这种di的取法称为线性探测再散列。即“下一个位置”为哈希表的直接后继。若当di=m-1时仍未查到,则说明表满,还要查找另外的溢出表。缺点:容易产生“二次聚集”
b)二次探测再散列
di=12,-12,22,-22,…,±k2 (k≤m/2)
c)伪随机探测再散列
di由一个伪随机函数发生器产生的一个伪随机数序列来确定。
(2)链地址法
将所有关键字为同义词的记录存储在同一链表中。设哈希地址在区间[0..m-1]上,设置一个指针向量:
Chain chainhash[m];
每个分量的初始状态为空,凡哈希地址为i的的记录则插入到chainhash[i]的链表中。插入的位置可以在表头、表尾,也可在中间。为了查找的方便,可以使同一链表中记录的关键字有序。如
K={19,14,23,01,68,20,84,27,55,11,10,79}
H(key)=keymod 13,存储链表如图中所示:
二、哈希表C语言描述
三、哈希表C语言实现
#include"stdio.h"
#include"stdlib.h"
#define SUCCESS 1
#define UNSUCCESS0
#define DUPLICATE-1
#define OK 1
#define ERROR -1
#define EQ(a,b)((a)==(b))
#define LT(a,b)((a)< (b))
#define LQ(a,b)((a)<=(b))
#define BT(a,b)((a)> (b))
#define NULLKEY-111
inthashsize[]={11,19,29,37}; // 哈希表容量递增表,
//一个合适的素数序列
int m=0; // 哈希表表长,全局变量
typedef intKeyType;
typedef int info;
typedef struct
{
KeyType key;
//info otherinfo;
}ElemType;
typedef struct
{
ElemType *elem;
int count;
int sizeindex;
}HashTable;
intInitHashTable(HashTable &H)
{ // 操作结果: 构造一个空的哈希表
int i;
H.count=0; // 当前元素个数为0
H.sizeindex=0; // 初始存储容量为hashsize[0]
m=hashsize[0];
H.elem=(ElemType*)malloc(m*sizeof(ElemType));
if(!H.elem)
exit(0); // 存储分配失败
for(i=0;i<m;i++)
H.elem[i].key=NULLKEY; // 未填记录的标志
return OK;
}
voidDestroyHashTable(HashTable &H)
{ // 初始条件: 哈希表H存在。操作结果: 销毁哈希表H
free(H.elem);
H.elem=NULL;
H.count=0;
H.sizeindex=0;
}//DestroyHashTable
int Hash(KeyTypeK)
{ // 一个简单的哈希函数(m为表长,全局变量)
//除留余数法
return K%m;
}//Hash
void collision(int &p,int d) // 线性探测再散列
{ // 开放定址法处理冲突
p=(p+d)%m;
}//collision
intSearchHash(HashTable H,KeyType K,int &p,int &c)
{
p=Hash(K); //构造哈希函数
while(H.elem[p].key!=NULLKEY&&!EQ(K,H.elem[p].key))
{
collision(p,++c); //冲突检测
if(c>=m) break;
}
if(EQ(K,H.elem[p].key))
return SUCCESS;
else returnUNSUCCESS;
}//SearchHash
intInsertHash(HashTable &H,ElemType e);
voidRecreateHashTable(HashTable &H) // 重建哈希表
{ // 重建哈希表
int i,count=H.count;
ElemType*p,*elem=(ElemType*)malloc(count*sizeof(ElemType));
p=elem;
printf("重建哈希表\n");
for(i=0;i<m;i++) // 保存原有的数据到elem中
if((H.elem+i)->key!=NULLKEY) // 该单元有数据
*p++=*(H.elem+i);
H.count=0;
H.sizeindex++; // 增大存储容量
m=hashsize[H.sizeindex];
p=(ElemType*)realloc(H.elem,m*sizeof(ElemType));
if(!p)
exit(-1); // 存储分配失败
H.elem=p;
for(i=0;i<m;i++)
H.elem[i].key=NULLKEY; // 未填记录的标志(初始化)
for(p=elem;p<elem+count;p++) // 将原有的数据按照新的表长插入到重建的哈希表中
InsertHash(H,*p);
}//RecreateHashTable
intInsertHash(HashTable &H,ElemType e)
{ // 查找不成功时插入数据元素e到开放定址哈希表H中,并返回OK;
// 若冲突次数过大,则重建哈希表
int c,p;
c=0;
if(SearchHash(H,e.key,p,c)) // 表中已有与e有相同关键字的元素
return DUPLICATE;
else if(c<hashsize[H.sizeindex]/2) // 冲突次数c未达到上限,(c的阀值可调)
{ // 插入e
H.elem[p]=e;
++H.count;
return OK;
}
else
RecreateHashTable(H); // 重建哈希表
return ERROR;
}
intInsertHashD(HashTable &H)
{
ElemType e;
printf("inputthe data until -1\n");
scanf("%d",&e.key);
while(e.key!=-1)
{
InsertHash(H,e);
printf("input the data until-1\n");
scanf("%d",&e.key);
}//while
return 1;
}//InsertHashD
intSearchHashD(HashTable &H)
{
KeyType key;
int p=0,c=0;
printf("inputthe data you want to search:\n");
scanf("%d",&key);
if(SearchHash(H,key,p,c))
printf("the location is%d,%d\n",p,H.elem[p].key);
elseprintf("Search Failed!\n");
return 1;
}//SearchHashD
void print(intp,ElemType r)
{
printf("address=%d (%d)\n",p,r.key);
void TraverseHash(HashTableH,void(*Vi)(int,ElemType))
{ // 按哈希地址的顺序遍历哈希表
printf("哈希地址0~%d\n",m-1);
for(int i=0;i<m;i++)
if(H.elem[i].key!=NULLKEY) // 有数据
Vi(i,H.elem[i]);
}//TraverseHash
voidTraverseHashD(HashTable &H)
{
TraverseHash(H,print);
}//TraverseHashD
int main()
{
HashTable H;
InitHashTable(H);
InsertHashD(H);
SearchHashD(H);
TraverseHashD(H);
DestroyHashTable(H);
return 1;
}
四、复杂度分析
从哈希表的查找过程可见:
1、虽然哈希表在关键字与记录的存储位置之间建立了直接映象,但由于冲突的产生,使得哈希表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需以平均查找长度作为衡量哈希表的查找效率的度量。
2、查找过程中需与给定值进行比较的关键字的个数取决于下面三种因素:
哈希函数
处理冲突的方法
哈希表的装填因子
哈希函数的好坏首先影响出现冲突的频繁程度
假定哈希函数是“均匀的”,即不同的哈希函数对同一组随机的关键字,产生冲突的可能性相同。
对同一组关键字,设定相同的哈希函数,则不同的处理冲突的方法得到的哈希表不同,它的平均查找长度也不同。
若处理冲突的方法相同,其平均查找长度依赖于哈希表的装填因子。
冲突的多少与表的填满程度有关,填满程度用α表示:
α=表中记录数/哈希表的长度
α标志哈希表的装满程度。
α越小,发生冲突的可能性越小,反之,α越大,表中已填入的记录越多,再填记录时,发生冲突的可能性越大。查找时,给定值需与之进行比较的关键字个数就越多,检索越慢。