哈希表:
在前面所学习的线性表和树中,记录在结构中的相对位置是随机的,即存储结构和记录之间不存在确定的关系。因此,在结构中查找某一个记录需要进行一系列和关键字的比较,这类查找方法建立在比较的基础上,查找的效率取决于查找过程中所进行的比较次数。而理想的情况是,需要查找某一个元素记录时,能够根据关键字和存储位置之间的某一种关确定关系直接找到这个存储位置,不需要通过比较,快速高效。这就是我们所要学习的哈希表。
哈希表的定义:
哈希表也叫散列表,它是一种可以根据关键字直接进行访问的数据结构。哈希表通过某种关系把关键字映射到表中一个位置,这样存储位置与关键字之间有一个对应的关系f,使得每个关键字key对应一个存储位置f(key)。这样在查找时,根据给定的关键字key,通过f(key)这一对应关系可快速确定包含key的记录在存储空间中的位置。
这个映射的函数f叫作哈希函数,又称为散列函数,按这个思路存储记录的连续空间称为散列表或哈希表。关键字对应的存储地址称为哈希地址或散列地址。
哈希表在存储时,以数据中每个元素的关键字key为自变量,通过哈希函数f(key)计算出函数数值,以该函数值作为一块连续存储空间的索引,将该元素存储到函数值指引的单元中。
哈希表存储的是键值对,其查找的时间复杂度与元素数量无关,在查找元素时是通过计算哈希码值来定位元素的位置从而直接访问元素的,因此,哈希表查找的时间复杂度为O(1)。哈希表的这种数据结构使得它可以提供快速的查找,插入和删除操作,无论哈希表中有多少数据,查找,插入和删除的时间复杂度都为O(1)。
当然哈希表也有一些缺点,它的存储是基于数组的,数组创建后难于扩展,当基本被填满时,性能将会大幅下降,所以必须清楚表中将要存储多少数据,而且它无法提供有序的遍历,不能进行某一范围的查找。
除此之外,哈希表还有一个冲突问题,在理想情况下,每一个关键字通过哈希函数计算出来的地址都是不一样的,可现实中,时常会碰见两个关键字key!=key2,但是却有f(key1)=f(key2),这种现象称为冲突,并把key1和key2称为这个哈希函数的同义词。
哈希函数的构建方法:
在构造哈希函数时,要使哈希地址尽可能均匀地分布在散列空间上,同时使计算结果尽可能简单,冲突次数减少。
1.直接定址法:
取关键字或关键字的某个线性函数值作为哈希地址,即f(key)=key或f(key)=a*key+b(a,b均为常数),这种哈希函数也叫作自身函数,如果f
(key)的哈希地址上已经有值,则往下一个位置查找,直到找到的f(key)的位置没有值,就把元素存放进去。
2.数字分析法
数字分析法是取数据元素关键字中某些取值较均匀的数字为作为哈希地址的方法,即当关键字的位数很多时,可以通过对关键字的各位进行分析,丢掉分布不均匀的位,作为哈希值。
3.平方取中法:
这是一种比较常用的哈希函数构造方法,这个方法是先取关键字的平方,然后再根据可使用空间的大小,选取平方数中的中间几位作为哈希地址。它的原理是通过取平方扩大差别,平方值的中间几位和这个数的每一位都相关,则对不同的关键字得到的哈希函数值不易产生冲突,由此产生的哈希地址也较为均匀,取的位数由表长决定。
平方取中法是最接近于“随机化”的构造方法,它一般适用于不了关键字分布而关键字位数又不是很多的情况。
3.折叠法:
所谓折叠法是将关键字分割为数位相同的几部分(最后一部分的位数可以不同),然后取这几位的叠加和(舍弃进位)作为哈希地址。这种方法适用于关键字位数较多,而且关键字中每一位上数字分布大致均匀的情况。
折叠法中的数位折叠又分为两种:移位叠加和边界叠加。移位叠加是将分割后每一部分的最低位对齐,然后相加;边界叠加是从一端向另一端延分界限来回折叠,然后对齐相加。
折叠法不需要事先知道关键字的分布,适合关键字位数较多的情况。
5.除留余数法:
假设哈希表的表长为m,则某一个小于等于m的数p作为关键字的除数,所得的余数作为哈希地址,即f(key)=key%p(p<=m),除数p称为模,这是一种最简单也最常用的哈希函数构建法。
除留余数法不仅可以对关键字直接取模,也可以在折叠,平方取中等运算后取模。在使用除留余数法时,模p的选择很重要,如果选择不当,容易产生同义词。一般情况下,p值可以为质数,或者不包含20以下质因数的合数。理论研究表明,除留余数法的模p取不大于表长且最接近表长m的质数为最好。
6.随机数法:
随机数法就是用随机函数获取一个随机值作为哈希地址,即f(key)=random(key)。random()是产生随机数的函数。当关键字长度不等时可以采用此方法来构造哈希函数。
处理哈希冲突:
1.开放定址法:
当关键字key的哈希地址p=f(key)出现冲突时,则以p为基础再产生另外一个哈希值p1=f(p),如果p1仍冲突,再以p1为基础产生p2,如此操作直到产生一个不冲突的哈希地址为止,然后将相应的元素存入其中。这种方法哈希函数如下图所示:
fi(key)=(f(key)+di)%m(di=1,2,3,4…,n) 其中,m是哈希表长,di为增量序列,增量序列的取值不同,相应的定址方式也不同。
因为di是线性增长的,所以也称为线性探测法。用线性探测法处理冲突,思路清晰,算法简单,但也有一些缺点,如处理溢出需要另外编写程序,删除工作比较困难等。
为此,可以改进增量序列di的值,令di=1^2,-1^2,2^2,-2^…(q<=m/2),增加的平方运算是为了不让关键字聚集在某一块区域。这样就等于是可以在更大的范围内双向寻找可能的空位。这种方法称为二次探测法:
fi(key)=(f(key)+di)%m(di=1^2,-1^2,2^2,-2^…(q<=m/2))
除了上述两种探测方法外,还可以将探测步长从常数改为随机序列,即令di的值取一个随机数,其对应的哈希函数如下所示:
fi(key)=(f(key)+RN)%m(RN是一个随机数)
2.再哈希法:
所谓再哈希法就是构造不同的哈希函数来求不同关键字的哈希地址。第一个数据元素用直接定址法来求哈希地址,第二个数据元素计算出来的哈希地址与第一个冲突了,那么就用平方取中法构造一个哈希函数来求哈希地址。
3.拉链法:
拉链法是将所有关键字为同义词的结点链接在同一个单链表中,若选定的哈希表长度为m,则可将哈希表定义为一个由m个头指针组成的指针数组T[0,m-1],凡是哈希地址为i(0<=i<=m-1)的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。
此哈希表中,互为同义词的数据元素都放在同一个单链表中,链表的地址存储在数组中,这就是链表地址法的来源,通常称它为拉链法。
拉链法处理冲突简单且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短。由于链表上的结点空间是动态申请的,故它更适合在创建哈希表前不知道表长的情况。用拉链法创建哈希表,在表中删除结点的操作更易于实现,只要简单地删除链表上的相应结点即可。
当然它也有不足之处,指针需要额外的空间,故当结点规模较小时,拉链法并不是很好的一个选择,而且在查找时需要遍历单链表,有一定的性能损耗。
4.创建公共溢出区:
另外创建一个表专门用来存储冲突的数据元素,假如一个关键字计算出的哈希地址中已经有数据,那么就将这个关键字存储到溢出表中。
哈希表的查找实现:
创建一个哈希表来存储一组数据{107,8,13,22,16,30,103,76,220,94},具体代码如下:
#include
#include
#define HASHSIZE 10
typedef struct
{
int* elem;
int count;
}HASHTable
int m=0;
void InitHashTable(HashTable * h)
{
int i=0;
m=HASHSIZE;
h->count=m;
h->elem=(int *)malloc(m*sizeof(int));
for(i=0;i<m;i++)
{
h->elem[i]=NULL;
}
return;
}
int Hash(int key)
{
return key%m;
}
void InsertHash(HashTable* h,int key)
{
int addr=Hash(key);
while(h->elem[addr]!=NULL)
addr=(addr+1)%m;
h->elem[addr]=key;
}
int SearchHash(HashTable h,int key,int *addr)
{
*addr=Hash(key);
while(h.elem[*addr]!=key)
{
*addr=(*addr+1)%m;
if(h.elem[*addr]==NULL||*addr==Hash(key))
return 0;
}
return 1;
}
int main()
{
HashTable ht;
InitHashTable(&ht);
int addr[10]={107,8,13,22,16,30,103,76,220,94};
for(int i=0;i<10;i++)
{
InsertHash(&ht,arr[i]);
}
int num=0;
printf("请输入要查找的数据:\n");
scanf("%d",&num);
int addr=Hash(num);
int ret=SearchHash(ht,num,&addr);
if(ret)
printf("查找成功!\n");
else
printf("查找失败!\n");
system("pause");
return 0;
}