查找算法的高取决于比较的次数。如果不经过比较就能确定要查找的元素的位置,那么查找效率就会大大提高,这就需要建立一种数据元素的关键字与数据元素存放地址之间的对应关系,通过数据元素的关键字直接确定其存放的位置,即哈希表,又称散列表。
1.哈希表的定义
理想的情况是希望不经过任何比较,一次存取便能得到所查记录,那就必须在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每个关键字和结构中一个唯一的存储位置相对应。因而在查找时,只要根据这个对应关系f找到给定值K的像f(K) 。若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上找到给定值K的像f(K)。若结构中存在关键字和K相等的记录,则必定在f(K)的存储位置上,由此不需要进行比较便可直接缺德所查记录。我们称这个对应关系f为哈希函数,也称为散列函数,按这个思想建立的表为哈希表,也叫散列表。
1. 哈希函数是一个映像,因此哈希函数的设定很灵活,只要使得任何关键字由此所得的哈希函数值都落在表长允许范围之内即可。
2. 对不同的关键字可能得到同一个哈希地址,即key1≠key2,而f(key1)=f(key2),这种现象称为冲突。具有相同函数值的关键字对该哈希函数来说称作同义词。
在一般情况下,冲突只能尽可能地少,而不能完全避免。因为哈希函数是从关键字集合到地址集合的映像。通常关键字集合比较大,它的元素包括所有可能的关键字,而地址集合的元素仅为哈希表中的地址值。在一般情况下,哈希函数是一个压缩映像,这就不可避免产生冲突。因此在建造哈希表时不仅要设定一个“好”的哈希函数,而且要设定一种处理冲突的方法。
也可这样描述哈希表:根据设定哈希函数H(key)和处理冲突的方法将一组关键字映像到一个有些爱你的连续的地址集上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为哈希表,这一映像过程称为哈希造表或散列,所得存储位置称为哈希地址或散列地址。
2.哈希函数的构造方法
若对于关键字集合中的任一个关键字,经哈希函数映像到地址集合中任何一个地址的概率是相等的,则称此类哈希函数为均匀的哈希函数。换句话说,就是使关键字经过哈希函数得到一个“随机的地址”,以便使一组关键字的哈希地址均匀分布在整个地址区间中,从而减少冲突。
常用的构造哈希函数的方法有:
1.直接定址法
取关键字或关键字的某个线性函数值为哈希地址。即H(key)=key或H(key)=a∗key+b,其中a和b为常数(这种哈希函数叫做自身函数)。
直接定址法的计算比较简单,且由于直接定址所得地址集合和关键字集合的大小相同而不会发生冲突。但由于这种方法产生的哈希函数地址十分分散,造成内容的大量浪费,因此一般不采用这种构造方法。
2.平方取中法
这种方法先求出关键字k的平方k2,然后取k2中间几位数字作为哈希地址。如何取哈希值的位数要根据B的大小来确定;为了限制越界,还要根据B的值设置一个适当的比例因子。
3.数字分析法
这种方法要在最初建立哈希表时已知全部或大部分数据的关键字,通过对它们进行全面的抽样分析,按照B的大小,选择数字分布均匀的若干位组成哈希地址。由于选择的数字是均匀分布的,因此哈希地址也是均匀分布的。
4.折叠法
这种方法把关键字截成几段,每段的长度由B的位数确定。然后,把每一段作为一个加数,求各段相加的和,作为哈希值。分段时,如果不能分成等长的几段,则剩余的低位数字作为一段。
相加时有两种方法:一种是顺叠,即把每一段的数字从高位到低位依次对齐进行求和,这种方法也称为移位法;另一种是对折,向折纸条一样,把原来关键字中的数字按照划分的界线向中间段折叠,然后求和,这种方法称为对折法。相加时,舍掉由高位产生的进位。
由于相加的和与关键字中的每位数字及其排列顺序相关,因此得到的散列值具有较好的随机性。
5.质数除余法
这种方法采用模(%)运算,即将关键字除以一个整数,取结果的余数作为哈希地址。在确定作为模的整数时,通常对于有B的桶的哈希表,取m≤B的最大质数作为模。
6.随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key)=random(key),其中random为随即函数。通常当关键字长度不等时采用此法构造哈希函数较恰当。
实际工作中需视不同的情况采用不同的哈希函数。通常考虑的因素有:
- 计算哈希函数所需的事件;
- 关键字的长度;
- 哈希表的大小;
- 关键字的分布情况;
- 记录的查找频率。
3.处理冲突的方法
假设哈希表的地址为0~(n-1),冲突是指由关键字得到的哈希地址为j(0≤j≤n−1)的位置上已存有记录,则处理冲突就是为该关键字的记录找到另一个“空”的哈希地址。在处理冲突的过程中可能得到一个地址序列Hi, i=1,2,…,k,(H∈[0,n−1])。即在处理哈希地址的冲突时,若得到的另一个哈希地址H1仍然发生冲突,则再求下一个地址H2,若H2仍然冲突,再求得H3。依此类推,直至Hk不发生冲突为止,则Hk为记录在表中的地址。
H2通常的处理冲突的方法有下列几种:
1. 开放定址法
其中:H(key)为哈希函数;m为哈希表表长;di为增量序列,可有下列3种取法:(1)di=1,2,3,…,m−1,称线性探测再散列;(2)di=12,−12,22,−22,33,…,±k2,(k≤m/2)称二次探测再散列;(3)di=伪随机探测再散列。
从线性探测再散列的过程中可以看到一个现象:当表中i,i+1,i+2位置上已填有记录时,下一个哈希地址为i、i+1、i+2和i+3的记录都将填入i+3的位置,这种在处理冲突过程中发生的两个第一个哈希地址不同的记录争夺同一个后继哈希地址的现象称作“二次聚集”,即在处理同义词的冲突过程中又添加了非同义词的冲突,显然这种现象对查找不利。但另一方面,用线性探测再散列处理冲突可以保证做到:只要哈希表未填满,总能找到一个不发生冲突的地址Hk,而二次探测再散列只有在哈希表长m为形如4j+3(j为整数)的素数时才可能,随机探测再散列,则取决于伪随机数列。
2.再哈希法
3.链地址法
将所有关键字为同义词的记录存储在同一线性链表中。假设某哈希函数产生的哈希地址在区间[0,m-1]上,则设立一个指针型向量Chain chainHash[m];其每个分量的初始状态都是空指针。凡哈希地址为i的记录都插入到头指针为ChainHash[i]的链表中。在链表中的插入位置可以在表头或表尾;也可以在中间,以保持同义词在同一线性链表中按关键字有序。
4.建立一个公共溢出区
假设哈希函数的置于为[0,m-1],则设向量HashTable[1…m-1]为基本表,每个分量存放一个记录,另设立向量OverTable[0…v]为溢出表。所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。
4.哈希表应用举例
例:给定一组元素的关键字hash[]={23,35,12,56,123,39,342,90},利用除留余数法和线性探测再散列法将元素存储在哈希表中,并查找给定的关键字,求平均查找长度。
算法实现狐妖包括构建哈希表、在哈希表中查找给定的关键字、输出哈希表及求平均查找长度。关键字的个数是8个,假设哈希表的长度m为11,p为11,利用除留余数法求哈希函数即h(key)=key%p,利用线性探测再散列法解决冲突即hi=(h(key)+di),哈希表如下图所示。
哈希表的查找过程就是利用哈希函数和处理冲突创建哈希表的过程。尽管利用哈希函数可以直接找到对应的元素,但是仍然会不可避免地有冲突产生,在查找的过程中,比较仍会是不可避免的,因此仍然以平均查找长度衡量哈希表查找的效率高低。假设每个关键字的查找概率都是相等的,则在哈希表中查找某个元素成功时的平均查找长度为ASL成功=18×(1×5+3+5+8)=2.625。
- 类型定义
#include<stdlib.h>
#include<stdio.h>
#include<malloc.h>
typedef int KeyType;
typedef struct /*元素类型定义*/
{
KeyType key; /*关键字*/
int hi; /*冲突次数*/
}DataType;
typedef struct /*哈希表类型定义*/
{
DataType *data;
int tableSize; /*哈希表的长度*/
int curSize; /*表中关键字个数*/
}HashTable;
- 创建操作
void CreateHashTable(HashTable *H,int m,int p,int hash[],int n)
/*构造一个空的哈希表,并处理冲突*/
{
int i,sum,addr,di,k=1;
(*H).data=(DataType*)malloc(m*sizeof(DataType)); /*为哈希表分配存储空间*/
if(!(*H).data)
exit(-1);
for(i=0;i<m;i++) /*初始化哈希表*/
{
(*H).data[i].key=-1;
(*H).data[i].hi=0;
}
for(i=0;i<n;i++) /*求哈希函数地址并处理冲突*/
{
sum=0; /*冲突的次数*/
addr=hash[i]%p; /*利用除留余数法求哈希函数地址*/
di=addr;
if((*H).data[addr].key==-1) /*如果不冲突则将元素存储在表中*/
{
(*H).data[addr].key=hash[i];
(*H).data[addr].hi=1;
}
else /*用线性探测再散列法处理冲突*/
{
do
{
di=(di+k)%m;
sum+=1;
} while((*H).data[di].key!=-1);
(*H).data[di].key=hash[i];
(*H).data[di].hi=sum+1;
}
}
(*H).curSize=n; /*哈希表中关键字个数为n*/
(*H).tableSize=m; /*哈希表的长度*/
}
- 查找操作
int SearchHash(HashTable H,KeyType k)
/*在哈希表H中查找关键字k的元素*/
{
int d,d1,m;
m=H.tableSize;
d=d1=k%m; /*求k的哈希地址*/
while(H.data[d].key!=-1)
{
if(H.data[d].key==k) /*如果是要查找的关键字k,则返回k的位置*/
return d;
else /*继续往后查找*/
d=(d+1)%m;
if(d==d1) /*如果查找了哈希表中的所有位置,没有找到返回0*/
return 0;
}
return 0; /*该位置不存在关键字k*/
}
- 平均查找长度
void HashASL(HashTable H,int m)
/*求哈希表的平均查找长度*/
{
float average=0;
int i;
for(i=0;i<m;i++)
average=average+H.data[i].hi;
average=average/H.curSize;
printf("平均查找长度ASL=%.2f",average);
printf("\n");
}
- 输出操作
void DisplayHash(HashTable H,int m)
/*输出哈希表*/
{
int i;
printf("哈希表地址:");
for(i=0;i<m;i++)
printf("%-5d",i);
printf("\n");
printf("关键字key: ");
for(i=0;i<m;i++)
printf("%-5d",H.data[i].key);
printf("\n");
printf("冲突次数: ");
for(i=0;i<m;i++)
printf("%-5d",H.data[i].hi);
printf("\n");
}
- 主程序
void main()
{
int hash[]={23,35,12,56,123,39,342,90};
int m=11,p=11,n=8,pos;
KeyType k;
HashTable H;
CreateHashTable(&H,m,p,hash,n);
DisplayHash(H,m);
k=123;
pos=SearchHash(H,k);
printf("关键字%d在哈希表中的位置为:%d\n",k,pos);
HashASL(H,m);
}
- 测试结果