关键字集合K到一个有限的连续的地址集(区间)D的映射关系H表示为
H(key):K->D,key∈K
K为主关键字集合,H称为哈希函数或散列函数。按哈希函数构建的表称为哈希表。D的大小m称为哈希表的地址区间长度。
例:假设要建立一个地址长度区间为13的哈希表。哈希函数为
H(key)=[Ord(关键字第一个字母)-1/2]
其中函数Ord为求字幕在字母表中的序号。例如A在字母表中的序号为1,Ord(‘A’)=1。现将关键字依次为Zhao、Qian、Sun、Li、Wu、Chen、Han的7个记录插入该哈希表。第一个关键字Zhao的哈希函数值为
H(Zhao)=[Ord(‘Z’)-1/2]=[(26-1)/2]=12
将Zhao存入112号单元。类似地,依次将各关键字映射到相应单元。
若要查找记录,则只需计算记录的关键字的哈希函数值,就可以直接找到该记录。
若在哈希表中插入关键字Xie,其哈希函数值为11,但十一号单元已被Wu占据,由此可见,不同关键字的哈希函数值可能相同,即
H(key1)=H(key2),Key1≠Key2
这种现象称之为冲突。哈希函数值相同的关键字称为同义字。
为了避免出现冲突,可以换一个哈希函数:
H(Key)=Ord(关键字第一个字母)%13
记录关键字对应的序列还是(Zhao、Qian、Sun、Li、Wu、Chen、Han)。若加入关键字Xie,则映射结果不会产生冲突。
但再加入关键字Dai,其哈希函数值为4,与Qian冲突。
在一般情况下,哈希函数是一种压缩映射,即关键字集的元素个数大于哈希地址集的元素个数,这就不可避免产生冲突。因此,在构建哈希表时不仅要设计一个好的哈希函数,而且要设定一个处理冲突的方法。
根据设定的哈希函数和处理冲突的方法将一组关键字映射到一个连续的有限地址集上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表称为哈希表,这一种映射称为哈希造表或散列,所得存储位置称为哈希地址或散列地址。
若对于关键字集合中的任一个关键字,经哈希函数映射到地址集合中任何一个地址的概率是相等的,则称此类哈希函数为均匀的哈希函数。哈希函数越均匀,发生冲突的概率越低。
哈希函数的构造方法有很多,设计时应注意两个原则:第一、计算过程尽量简单。
第二、哈希函数尽量均匀。
最简单的直接定址法直接用关键字key作为哈希地址,即H(key)=key。
例:
要查找11岁的儿童人数时,直接定位第11项即可。
虽然这种哈希函数简单,且不会产生冲突,但在实际应用中,关键字很少是连续的,采用这种特殊哈希函数会造成哈希表空间浪费。一般情况下,直接定址法可通过对关键字缩放和平移,获得和是的地址空间,即线性函数
H(key)=a×key+b
其中,a为缩放函数,b为平移系数。
算法:直接定址的哈希函数
int hash_d(int key){
return a*key+b;//a为缩放函数,b为平移系数
}
例如某公司在职员工各年龄人数统计,年龄从18周岁到65周岁,就可以采用将关键字年龄减去18来作为数据的存储单元,即a=1,b=-18。
对于地址区间长度为m的哈希表,除留余数法取某个不大于m的数p为模,将哈希函数定义为H(key)=key%p(p≤m)。
除留余数法简单常用,不仅可以对关键字直接取模,也可以在折叠、平方取中等运算后再取模。值得注意的是,模p的选择十分重要。当模p取不大于m且最接近m的素数,或不包含小于20的质因子的合数时,可使哈希地址尽可能均匀地分布再地址空间。
算法:除留余数地哈希函数
int hash_m(int key){
return key%p;
}
为保证求得地哈希函数值再地址区间长度范围以内,其他哈希函数构造方法常把除留余数法作为其最后一步运算。
当关键字地位数很多时,可以通过对关键字地各位进行分析,去掉分布不均与地位,将分布均匀的位提取出来作为哈希值。数学分析法取关键字中某些取值较均匀的数位作为哈希地址,适合于关键字位数较多,且能预测关键字各位分布均匀度的情况。
例:构造一个数据元素个数n=60,哈希地址空间长度m=100的哈希表。假设关键字均为8为十进制数。
对于全部60个关键字分析发现,关键字从左到右的第1、2、3、6位取值较均匀,可选取其中的两位作为哈希地址。若选取最后两位作为哈希地址,则以上八个关键字的哈希地址分别为02、75、28、34、15、38、62、20。
显然数字分析法仅适用于事先直到表中所有关键字每一位数据的分布情况,它完全依赖于关键字集合。
折叠法将关键字分割成位数相同的 若干部分(最后一部分的位数可以较少),并取各部分的叠加和(舍去进位)作为哈希函数。折叠法适用于关键字位数较多且每一位分布大致均匀的情况。
分割后,可采用移位叠加或Z形叠加:移位叠加是将分割后每一部分的最低位对其,然后相加;Z形叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
算法:移位叠加的哈希函数
int hash_s(long key)
{
int i,j=1,qu,sum=0;
for(i=0;i
设哈希表的地址区间长度为10000,关键字key=401108105302169891,允许的地址空间为4位十进制数,w=4,则这两种叠加情况为:
用移位叠加得到的哈希地址是2308,而用Z形叠加所得到的哈希地址为5115。
平均取中法先取关键字的平方,然后根据哈希表地址区间长度m的大小,选取平方数的中间若干位作为哈希地址。该方法通过取平方扩大关键字之间的差别,而平均值的中间若干位和这个数的每一位都相关,使得不同关键字的哈希函数值分布较为均匀,不易产生冲突。
设哈希表地址区间长度为1000,可取关键字平方值的中间3位。
算法:平方取中法的哈希函数算法
int hash_3(int key){
long temp;
temp=key*key/100;
if(temp>=1000)//关键字平方值大于5位
{
temp-=temp/1000*1000;
}
return temp;
}
一般情况下,应该根据关键字的取值范围,来确定取关键字平方值的中间哪几位作为哈希地址。
构造哈希表时,应根据关键字集合的特点选择合适的哈希函数,是哈希地址尽量均匀地分布在哈希地址区间内,以免出现或减少冲突。但哈希函数地构造与关键字地长度,哈希表地大小,关键字地实际取值状况等许多因素有关,而且有些因素事前不能确定,所以冲突不易避免,关键在于如何处理。
链地址法将关键字为同义词地记录链接在同一单链表中。设哈希表地址区间长度为m,则可将哈希表定义为一个由m个头指针组成的指针数组T[0…m-1]。凡是哈希地址为i的记录,均插入以T[i]为头指针的单链表中,称该单链表为i同义单链表.T中各分量的初值置为空指针.
假设有8个关键字22,41,53,46,30,13,12,67,哈希表的地址区间长度为11,哈希函数为H(key)=(3*key)%11,采用链地址法,将记录插入其同义词链表的表头.
开放定址法是哈希表的地址空间内解决冲突.插入时一旦发生冲突,可使用某种探测技术在哈希表中计算得到另一个地址,若不冲突,则插入,否则求下一个地址,直到探测到空闲地址为止,插入新记录到该地址单元.在探测过程中,求得的一系列地址称为探测地址序列.
查找的探测过程与插入相同.沿着探测地址序列逐个查找,若找到给定的关键字,则查找成功;若探测到一个空闲地址,则表明表中无待查的关键字,查找失败.
两种常用的开放定址法:线性探测法和二次探测法.
线性探测法的基本思想是,假定哈希函数为H(key),哈希函数的地址区间长度为m,并将哈希表看成是一个循环空间,则线性探法的探测地址序列可表示为
Hi=(H(key)+i)%m i≤i≤m-1
其中,Hi表示第i次冲突时探测的地址空间.
假设有8个关键字22,41,53,,46,30,13,12,67,哈希表的地址区间长度为11,哈希函数为H(key)=(3*key)%11,采用线性探测法处理冲突,可求得各关键字的哈希地址:
H(22)=0;
H(41)=2;
H(53)=5;
H(46)=6;
H(30)=2 H1(30)=3
H(13)=6 H1(13)=7
H(12)=3 H1(12)=4
H(67)=3 H1(67)=4 H2(67)=5 H3(12)=6 H4(67)=7 H5(67)=8
用线性探测法处理冲突,思路清晰,算法简单,但线性探测法很容易产生堆聚现象,就是存入哈希表的记录在表中连成一片.按照线性探测法处理冲突,如果堆聚的记录越多,则发生冲突是=时的探测次数越多.
二次探测法生成的探测地址序列不是连续的,而是跳跃式的,为后续待插入的记录留下空间从而减少堆聚.二次探测法的探测地址序列可表示为
Hi=(H(key)+di)%m 1≤i≤m-1
其中,di=12,-12,22,-22,…,k2,-k2(k≤m/2),即
H=H(key)
H1=(H+12)%m
H2=(H-12)%m
H3=(H+22)%m
H4=(H-22)%m
…
还是上面8个关键字22,41,53,,46,30,13,12,67,哈希表的地址长度为11,哈希函数仍为H(key)= (3*key)%11,但采用二次探测法处理冲突,可求得各关键字的哈希地址.
H(22)=0
H(41)=2
H(53)=5
H(46)=6
H(30)=2 H1(30)=3
H(13)=6 H1(13)=7
H(12)=3 H1(12)=4
H(67)=3 H1(67)=4 H2(67)=2 H3(12)=7 H4(67)=10
采用链地址哈希表处理冲突的哈希表称为链地址哈希表,其类型定义如下:
typedef struct Node {
RcdType r;
struct Node* next;
}Node;
typedef struct {
Node** rcd;
int size;//哈希表容量
int count;//当前表中含有的记录个数
int (*hash)(KeyType key,int hashSize)//函数指针变量,选取的哈希函数
}HashTable;
链地址哈希表的接口定义如下:
Status InitHash(HashTable &H,int size,int(*hash)(KeyType,int));//初始化哈希表
Status DestroyHash(HashTable& H);//销毁哈希表
Node* SearchHash(HashTable H, KeyType key);//查找
Status InsertHash(HashTable& H, RcdType e);//插入
Status DeleteHash(HashTable& H, KeyType key, RcdType& e);//删除
该操作构造一个初始容量为size的哈希表H,并对每个元素赋初值为空指针。链地址哈希表的初始化实现算法如下:
算法:链地址哈希表的初始化
Status InitHash(HashTable& H, int size, int (*hash)(TeyType, int))
{
//初始化哈希表
int i;
H.rcd = (Node**)malloc(size * sizeof(Node*));//分配长度为size的存储空间,元素类型为指针Node*
if (NULL == H.rcd)
{
return OVERFLOW;//分配失败
}
for (i = 0; i < size; i++)
{
H.rcd[i] = NULL;
}
H.size = size;
H.hash = hash;
H.count = 0;
return OK;
}
该操作在哈希表H中查找关键字为定值key的记录。若查找成功,返回该结点的指针,否则返回空指针。
算法:链地址哈希表的查找
int hash(int key, int hashSize) {//哈希函数,hashSize为空间长度
return (3 * key) % hashSzie;
}
Node* SearchHash(HashTable & H, int key) {//在哈希表H中查找关键字为key的记录
int p = H.hash(key, H.size);
Node* np;
for (np = H.rcd[0]; np != NULL; np = np->next)
{
if (np->r.key == key)
{
return np;
}
return NULL;
}
}
该操作唉哈希表H中插入记录e。查找关键字为e.key的记录,若已存在,则不插入,返回ERROR;否则将记录e插入其同义词链表的表头。
算法:
Status InsertHash(HashTable& H, RcdType e)
{
//在哈希表H中插入记录e
int p;
Node* np;
if ((np = SearchHash(H, e.key)) == NULL) {//查找不成功时插入
p = H.hash(e.key, H.size);
np = (Node*)malloc(sizeof(Node));
if (NULL == np)
return OVERFLOW;
np->r = e;
np->next = H.rcd[p];//插入到表头
H.rcd[p] = np;
H.count++;
return OK;
}
else
return ERROR;
}
采用开放定址法处理冲突的哈希表称为开放定址哈希表,其类型定义如下:
typedef struct{
RcdType *rcd;//记录存储地址,动态分配数组
int size;//哈希表容量
int count;//当前表中含有的记录个数
in *tag;//标记,0:空;1:有效;-1:已删除
int (*hash)(KeyType key,int hashSize);//函数指针变量,选取的哈希函数
void(*collision)(int &hashValue,int hashSize);//函数指针变量,用于处理冲突的函数
}HashTable;
开放定址哈希表的接口定义如下:
Status DestroyHash(HashTable &H);//销毁哈希表
Status CreatHash(HashTable &H);//构造哈希表
Status SearchHash(HashTable H,KeyType key,int &p,int &c);//在哈希表H中查找关键字为key的记录
int InsertHash(HashTable &H,RcdType e);//在哈希表H中插入记录e
Status DeleteHash(HashTable &H,KeyType key,RcdType &e);//在哈希表H中删除关键字为key的记录
Status InitHash(HashTable &H,int size,int(*hash)(KeyType,int),void(*collision)(int &,int));//初始化哈希表
该操作构造一个初始容量为size的哈希表H,并将哈希表中的记录标记域值为空。
算法:开放定址哈希表的初始化
Status DeleteHash(HashTable &H,KeyType key,RcdType &e){//在哈希表H中删除关键字为key的记录
int i;
H.rcd=(RcdType*)malloc(size*sizeof(RcdType));//分配长度为size的存储空间
H.tag=(int *)malloc(size*sizeof(int));//分配存储空间对应的标记数组
if(NULL==H.rcd||NULL==H.tag)
{
return OVERFLOW;//开辟失败
}
for(i=0;i
该操作是在哈希表H中查找关键字为给定值key的记录。若查找成功,令参数p指示待查记录在表中的位置,并返回SUCCESS;否则,令p指示该记录可插入的位置,并返回UNSUCCESS。参数c用于统计冲突发生次数,当达到某一阈值(小于哈希表地址区间长度),则需要重新构造哈希表。
算法:开放定址哈希表查找
Status SearchHash(HashTable H,KeyType key,int &p,int &c)//在哈希表H中查找关键字为key的记录
{
p=H.hash(key,H.size);//求得哈希地址
while((1==H.tag[p]&&H.rcd[p].key!=key)||-1==H.tag[p])
{
H.collision(p,H.size);//求得下一探测地址p
c++;
}
if(H.rcd[p].key==key&&1==H.tag[p])
{
return SUCCESS;//查找成功
}
else
{
return SUCCESS;
}
}
算法:线性探索法处理冲突
void collision(int &hashValue,int hashSize){
hashValue=(hashValue+1)%hashSize;
}
该操作在哈希表H中插入记录e。查找关键字为e.key的记录,若已存在,则不插入,返回-1;否则插入并返回查找时发生的冲突次数。
算法:开放定址哈希表的插入
int InsertHash(HashTable &H,RcdType e){//在哈希表H中插入记录e
int c=0;j;
if(SUCCESS==SearchHash(H,e.key,j,c))
{
return -1;//表明已有与e.key相同的关键字的记录
}
else{//插入记录e
H.rcd[j]=e;
H.tag[j]=1;
++H.count;
return c;
}
}
在实际应用中,若冲突次数超过阈值,可能是因为插入记录过多,需要扩容哈希表,也可能是哈希表函数选取不当,需要更换哈希函数,重新构造哈希表。
该操作在哈希表H中删除关键字为key的记录。查找关键字为key的记录,若不存在,则返回UNSUCCESS;否则删除该记录,并返回SUCCESS。
假设8个关键字22,41,53,,46,30,13,12,67,哈希表的地址区间长度为11,哈希函数为H(key)= (3*key)%11,若需要删除关键字为30的记录,计算得到的哈希地址为2,该单元的值为41,不是30;探测下一个哈希地址为3,正好是30,查找成功,置H.tag[3]为删除标记-1表示删除。若置H.tag[3]为空闲标记0,若此时查找关键字为67的记录,计算得到的哈希地址为3,则发现该单元空闲,查找失败。但关键字67由于与关键字30,12,53,46和13有冲突放到了8号单元,而并非不存在,因此在删除操作中,被删除的tag域应设为删除标记-1。
算法:开放定址哈希表的删除
Status DeleteHash(HashTable &H,KeyType key,RcdType &e){
int j,c;
if(UNSUCCESS==SearchHash(H,key,j,c))
{
return UNSUCCESS;//表明哈希表中不存在关键字为key的记录
}
else{
e=H.rcd[j];//被删除的记录
H.tag[j]=-1//删除标记
H.count--;
return SUCCESS;
}
}
对一组关键字,设定相同的哈希函数,若采用不同的处理冲突的方法,则构造出来的哈希表不同,他们的平均查找长度也不同。假设8个关键字22,41,53,,46,30,13,12,67,哈希函数为H(key)= (3*key)%11,地址区间长度为11.若采用链地址法处理冲突,找到67则只需要比较1次。若采用线性探测法处理冲突,查找67的比较次数为6.若采用二次探测法,同样查找67,则需要比较5次。
在概率查找成功的情况下,3种不同处理冲突的哈希表的平均查找长度不同。
在构造哈希表时,哈希表插入的记录数栈地址区间长度的比率称为装填因子α
α=表中填入的记录数/地址区间长度
一般情况下,处理冲突方法相同的哈希表,其平均查找长度依赖于α,直观上来看,α越大,发生冲突的可能性就越大,可以证明:
采用链地址法的哈希表查找成功时的平均查找长度为
Snc≈1+α/2
采用线性探测法的哈希表查找成功时的平均查找长度为
Snl≈1/2(1+1/(1-α))
采用二次探测法的哈希表查找成功时的平均查找长度为
Snr≈-1/αln(1-α)
由于哈希表中查找不成功时所需比较的次数与给定值有关,则可类似地定义哈希表中查找不成功时平均查找长度为查找不成功时需和给定值进行比较地关键字个数地期望值。同样可以证明,不同处理冲突的方法构成的哈希表在查找不成功时的平均查找长度分别为
Unr≈α+e-α——链地址法
Unl≈1/2(1+1/(1-α)2)——线性探测法
Unr≈1/(1-α)——二次探测法
这三种处理冲突的方法中,装填因子与平均查找长度在查找成功和查找不成功的关系如图(为了方便对比,取0≤α≤0.9),在装填因子较小时(<0.6时),这三种处理冲突的方法性能相差不大;但装填因子大于0.6后,线性探测处理冲突的性能极具下降;而链地址法处理冲突随着装填因子的变化,其性能变化不大。
链地址法与开放定址法各有特点及适用情形
对于预先知道且规模不大的关键字集,有时可以构造完美哈希函数。完美哈希函数是指没有冲突的哈希函数,即若K的大小为n,D的大小为m,m≥n,且哈希函数H不出现同义词。若m=n,则称为最小完美哈希函数(MPHF)