哈希散列技术:存储位置=f(关键字)
这样就可以通过查找关键字不需要比较就可以获得需要的记录的存储位置。
哈希技术是在记录的存储位置和他的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值key的映射f(key)。
把对应关系f称作散列函数,又称哈希函数。采用散列技术将记录存储在一块连续的储存空间中,这块连续的存储空间称为散列表或哈希表,关键字对应的记录存储位置称为散列地址。
哈希表查找步骤
(1)存储时,通过散列函数计算记录的散列地址,并按照此散列地址存储该记录。
(2)查找记录时,通过散列函数计算记录的散列地址,按照此散列地址来访问记录。
因此散列技术既是一种存储技术,也是一种查找技术。它与线性表,二叉树,图等数据结构不同的是,他们的元素之间都存在某种逻辑关系,而散列技术的记录之间不存在什么逻辑关系,只与关键字有关,因此散列技术主要是面向查找的存储结构。
设计一个简单,均匀,存储利用率高的散列函数是散列技术中最关键的问题。
冲突:2个关键字key1!=key2,但却有f(key1)=f(key2),这就是冲突,并把key1和key2称为散列函数的同义词。
散列函数构造方法
设计散列函数的2个原则
(1)计算简单:散列函数的计算时间不应该超过其他查找技术与关键字比较的时间。
(2)散列地址分布均匀:解决冲突的最好办法就是尽量让散列地址均匀分布在存储空间,这样可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。
1 直接定址法
f(key)=key;
f(key)=a*key+b; (a,b为常数)
就是取关键字的某个线性函数值为散列地址。
优点:简单均匀,不会产生冲突。
缺点:需要事先知道关键字分布情况,适合查找较小且连续的情况。
现实生活中不常用。
2 数字分析法
关键字的位数较多时,我们可以抽取关键字的一部分来计算散列存储地址。
譬如:手机号码作为关键字时,由于手机号前7位重复的概率比较大,采用前7位来计算哈希地址很容易冲突,因此可以直接选取后四位,也可以对后四位进行一些转换(比如反转,右位环移等)目的就是提供一个散列函数,可以合理地将关键字分配到散列表的 各位置。
该方法适用于处理关键字位数比较大的情况,如果事先知道关键字的分布而且关键字的若干位分布比较均匀可以采用。
3 平方取中法
顾名思义,可以把关键字求取平方,然后对结果选取中间几位数作为散列地址。
该方法适用于不知道关键字的分布,而位数又不是很大的情况。
4 折叠法
折叠法指将关键字从左到右分割为位数相等的几部分(最后一部分位数不够时可以短些),然后叠加求和,按照散列表的表长(这里可以理解为散列表的地址范围的位数?),取后几位作为散列地址。
比如关键字9876543210,散列表表长为3位,分为四组:987 654 321 0叠加求和位1566,求后三位为566即散列地址。
该法适用于事先不知道关键字分布,适合关键字位数比较多的情况。
5 除留取余法
对散列表表长为m(可以理解为散列表最多容纳的数据的个数?)的散列函数公式;
f(key)=key mod p(p<=m)
mod为取余,事实上该方法不仅可以对关键字直接取模,也可以在折叠,平方取中后再取模。该方法的关键在于选择合适的p,p如果选择的不好,可能会产生同义词。
根据经验,如果散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
6 随机数法
取关键字的随机函数值为它的散列地址。
f(key)=random(key)
这里random是随机函数。当关键字长度不等时,采用这个方法构造散列函数比较合适。
综上
应该视不同情况采用不同的散列函数,通常需要考虑:
(1)计算散列地址所需要的时间
(2)关键字长度
(3)散列表大小
(4)关键字分布情况。
(5)记录查找的频率。
处理散列冲突的方法
1 开放定址法
所谓开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总可以找到,并将记录存入。
这种每次增加一个常数的探测方法叫做线性探测法。但可能会出现本来不是同义词却需要争夺一个地址的情况,即堆积。
为了不让关键字都聚集再某一个区域,可以增加平方运算,即二次探测法
。
还有一种称为随机探测法:在冲突的时候,对于位移量di采用随机函数计算获得。
di是随即数列。
2 再散列函数法
对散列表来说,其实我们可以准备多个散列函数,当发生地址冲突时,可以换一个散列函数计算。
3 链地址法
可以将关键字为同义词的记录存储在一个单链表中,这种表为同义词子表,在散列表中只存储同义词字表的头指针。
这时候根本不会存在冲突,因为无论有多少个冲突,都只是在当前位置给单链表增加节点。但是却带来了查找时需要遍历单链表的性能损耗。
4 公共溢出区法
把所有冲突的记录按照冲突先后顺序都存放在另外一张表中,即溢出区。在查找时,对给定的值通过散列函数计算出散列地址后,先与基本表相应位置比较,如果相同,查找成功,否则取溢出表中顺序查找。
显然这适用于冲突较少的情况。
散列表查找的C++实现
#include <cstdio>
const int max_length=12;
int *hashtable=new int[max_length];
int cur_length=0;
int Hash(int key)
{
return key%12;
}
//插入关键字进入散列表
int inserthash(int hashtable[],int key)
{
int addr=Hash(key);
int step=0;
while(hashtable[addr]!=-1)
{
addr=(addr+1)%12;
step++;
if(step>=max_length)
{
return -1; //插入失败
}
}
hashtable[addr]=key;
return 1; //插入成功
}
//在散列表中查找关键字的地址,这里返回下标
int searchinhash(int hashtable[],int key)
{
int addr=Hash(key);
while(hashtable[addr]!=key)
{
addr=(addr+1)%12;
if(hashtable[addr]==-1||addr==Hash(key)) //如果循环到原点或者说找到位置但是为空,说明没有关键字
return -1; //查找失败
}
return addr; //返回查找地址
}
int main()
{
for(int i=0;i<max_length;i++)
{
hashtable[i]=-1;
}
int a[12]={12,67,56,16,25,37,22,29,15,47,48,34};
for(int i=0;i<12;i++)
{
int status=inserthash(hashtable,a[i]);
if(status==-1)
{
printf("元素%d插入失败!\n",a[i]);
}
else
{
printf("元素%d插入成功!\n",a[i]);
}
}
for(int i=0;i<12;i++)
{
int addr=searchinhash(hashtable,a[i]);
if(addr==-1)
printf("不存在元素%d\n",a[i]);
else
printf("元素%d的位置下标为%d\n",a[i],addr);
}
int status=inserthash(hashtable,100);
if(status==-1)
{
printf("元素100插入失败!\n");
}
else
{
printf("元素100插入成功!\n");
}
int addr=searchinhash(hashtable,99);
if(addr==-1)
printf("不存在元素99\n");
else
printf("元素99的位置下标为%d\n",addr);
delete []hashtable;
return 0;
}
散列表查找性能分析
如果没有冲突,散列表的查找性能为O(1)
散列表查找的平均长度取决于:
(1)散列函数是否均匀
散列函数的好坏直接影响着出现冲突的频繁程度。
(2)处理冲突的方法
相同的关键字,相同的散列函数,但处理冲突的方法不同,会使得平均查找长度不同。比如线性探测处理冲突可能会产生堆积,显然没有二次探测好,而链地址处理冲突不会产生任何堆积,因而具有更佳的平均查找性能。
(3)散列表的装填因子
所谓散列表的装填因子t=填入表中的个数/散列表长度。t标志着散列表装满的程度。当表中的记录越多,t越大,产生冲突的可能行越高。也就是说散列表的平均查找长度取决于装填因子,而不是查找集合中记录的个数。
不管记录个数多大,我们总可以选择一个合适的装填因子使平均查找长度限定在一个范围内,这时查找的时间复杂度真的是O(1)。