散列表(哈希表)查找
概述
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。
这种对应关系f称为散列函数,又称为哈希函数
采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表
关键字对应的记录存储位置称为散列地址
散列技术既是一种存储方法也是一种查找方法,散列技术的记录之间不存在什么逻辑关系,它只与关键字有关,散列主要是面向查找的存储结构。
散列技术最适合的求解问题是查找与给定值相等的记录。不适合一个关键词对应多个记录的情况,不适合范围查找。
当两个关键字不相等key1与key2,但是其f(key1) = f(key2)时,这种现象称为冲突,并把key1与key2称为散列函数的同义词
构造方法
标准
1.计算简单 2.散列地址分布均匀
一、直接定址法
取关键字的某个线性函数值为散列地址
需要事先知道关键字的分布情况,适合查找表比较小且连续的情况
二、数字分析法
抽取关键字的一部分来计算(反转、右环移、左环移等等)散列的存储位置。
适合处理关键字位数较大的情况,如果事先知道关键字的分布且若干关键字的若干位分布较均匀,可以使用这种方法。
三、平方取中法
将关键字平方,然后取其中间的几位当作散列地址
适合不知道关键字的分布,而位数又不是很大的情况
四、折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些) ,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
适合事先不需要知道关键字的分布,适合关键字位数较多的情况。
五、除留余数法
此方法为最常用的构造散列函数方法。 对于散列表长为 m 的散列函数公式为:
为减少冲突,根据经验经验,若散列表表长为 m,通常 p 为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
六、随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key)=random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
决定方法的因素
1.计算散列地址所需的时间
2.关键字的长度
3.散列袤的大小
4.关键字的分布情况
5.记录查找的频率
处理散列冲突方法
一、开放定址法
一旦发生了冲突 , 就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
1.线性探测法
上述解决冲突的开放定址法称为线性探测法,即di线性增加 。可能出现本来都不是同义词却需要争夺一个地址的情况,称为堆积
2.二次探测法
增加平方运算的目的不是为了不让关键字都聚集在某一块区域,这种方法为二次探测法
3.随机探测法
在冲突时,对于位移量 d,采用随机函数计算得到,我们称之为随机探测法。注意这里的随机数为伪随机数,使用相同的随机种子
二、再散列函数法
事先准备多个随机函数,每当发生散到地址冲突肘,就换一个散列函数计算,相信总会有一个可以把冲突解决掉。这种方法能够使得关键字不产生聚集,当然,相应地也增加了计算的时间。
三、链地址法
将所有的冲突的记录存储再同一个单链表中,如下图所示,能够保障绝对不会出现找不到地址的保障,但是也带来了查找时需要遍历单链表的性能损耗。
四、公共溢出区法
所有冲突的关键字建立了一个公共的溢出区来存放.
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功。如果不相等,则到溢出表去进行溢出表查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
散列表查找实现
以除留余数法和开放定址法为例,程序如下
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义哈希表的长为数组的长度
#define NULLKEY -32768
//结构定义
typedef struct
{
int *elem; //数据元素存储基址,动态分配数组
int count; //当前数据元素个数
}HashTable;
int m=O; //散列表长 全局变量
//哈希表初始化
Status InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE; //初始化表长
H->count = m; //初始化元素个数
H->elem = (int*) malloc(m*sizeof (int)); //根据表的大小开辟空间
for (i=O; i<m; i++)
H->elem[i] = NULLKEY;
return OK;
}
//散列函数
int Hash(int key)
{
return key % m; //除留余数法
}
//插入关键字到哈希表中
void InsertHash(HashTable *H,int key)
{
int addr = Hash(key); //求散列地址
while(H->elem[addr] != NULLKEY) //如果不为空则冲突
{
addr = (addr+1) % m; //开放定址法的线性探测
}
H -> elem[addr] = key; //直到有空位后插入关键字
}
//散列表查找关键字
Status SearchHash(HashTable H,int key,int *addr)
{
*addr = Hash(key); //求散列地址
while(H.elem[*addr] != key) //若不为空,则冲突
{
if(H.elem[*addr] == NULLKEY||*addr == Hash(key))
{
return UNSUCCESS; //如果循环到原点证明关键字不存在
}
}
return SUCCESS;
}
性能分析
如果没有冲突,则散列查找时间复杂度为O(1),但是如果有冲突,散列的查找平均长度取决于以下因素
1.散列函数是否均匀
散列函数的好坏直接影响着出现冲突的频繁程度
2..处理冲突的方法
相同的关键字、相同的散列函数,但处理冲突的方法不同,会使得平均查找长度不同。比如线性探测处理冲突可能会产生堆积,显然就没有二次探测法好,而链地址法处理冲突不会产生任何堆积,因而具有更佳的平均查找性能。
3.散装表的装填因子
所谓装填因子 = 填入表中的记录个数/散列表的长度。即表示这散列表的装满程度,越大代表冲突的可能性越大。散列袤的平均查找长度取决于装填困子,而不是取决于查找集合中的记录个数。
因此,不管记录个数n有多大,我们总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内,此时我们散列查找的时间复杂度就真的是 O(1)了。即浪费空间来换来查找效率的提升。