一、散列表
概念
散列技术是在记录的存储位置和它的关键字之间建立-一个确定的对应关系f, 使得每个关键字key 对应一个存储位置f (key)。
通过某个函数使得,存储位置=f(关键字)
这里我们把这种对应关系f称为散列函数,又称为哈希(Hash) 函数。按这个思想,采用散列技术将记录存储在一块连续的存储空间中, 这块连续存储空间称为散列表或哈希表(Hash table)。那么关键字对应的记录存储位置我们称为散列地址。
查找过程
-
在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
-
当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。由于存取用的是同一个散列函数,因此结果当然也是相同的。
特点
散列函数既是存储方法也是查找方法。其不适合用来范围查找,最适合的求解问题是查找与给定值相等的记录。
另一个问题是冲突。在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,我们会碰到两个关键字key1≠key2,但是却有f (key1) =f (key2),这种现象我们称为冲突(ollision), 并把key和key2称为这个散列函数的同义词(synonym)。
二、散列函数的构造方法
两个原则 :计算简单,散列地址分布均匀。
直接定址法
我们可以取一个线性函数,f(key)=a*key+b (a,b为常数)
例如:统计80后的人口数,我们可以用出生年份关键字减去1980
这样的散列函数优点就是简单、均匀,也不会产生冲突,但问题是这需要事先知道关键字的分布情况,适合查找表较小且连续的情况。
数字分析法
使用关键字的一部分,来计算散列存储位置。
例如:某员工登记表
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。
平方取中法
这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。再比如关键字是4321, 那么它的平方就是18671041,抽取中间的3位就可以是671,也可以是710,用做散列地址。
平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。
折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如:我们的关键字是9876543210,散列表表长为三位,我们将它分为四组987|654|321|0,然后将它们叠加求和987+654+321+0=1962,再求后3位得到散地址为962。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
除留余数法
取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。
因此根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
随机数法
选择一随机函数,取关键字的随机值作为散列地址,H(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。
三、处理散列冲突的方法
开放定址法
Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列
有下列三种取法:
- di=1,2,3,…,m-1,称线性探测再散列
- di=12,(-1)2,(2)2,(-2)2,±(k)2,(k<=m/2),称二次探测再散列
- di=伪随机数序列,称伪随机探测再散列
再散列法
Hi=RHi(key),i=1,2,…,k, RH,H均是不同的散列函数
即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
链地址法
将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针。
链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,这也就带来了查找时需要遍历单链表的性能损耗。
公共溢出区法
这个方法其实就更加好理解,为所有冲突的关键字建立了一个公共的溢出区来存放。
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功。如果不相等,则到溢出表去进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
四、散列表查找算法实现
#define success 1
#define unsuccess 0
#define hash_size 12//散列表长度
#define NULLKEY -32768
typedef struct
{
int* elem;//数据元素存放基址,动态分配数组
int count;//数据个数
}hash_table;
int m = 0;//散列表表长
bool init_hash_table(hash_table *h)//初始化
{
m = hash_size;
h->count = m;
h->elem = (int *)malloc(m * sizeof(int));
for (int i = 0; i < m; i++)
h->elem[i] = NULLKEY;
return true;
}
int Hash(int key)//散列函数
{
return key % m;
}
void insert_hash(int key,hash_table * h)
{//插入
int addr = Hash(key);
while (!h->elem[addr] == NULLKEY)
addr = (addr + 1) % m;
//开放定址法的线性探测(也可以用其他)
h->elem[addr] = key;
}
int search_hash(hash_table* h, int key,int * addr)
{
*addr = Hash(key);
while (h->elem[*addr] != key)
{
*addr = (*addr + 1) % m;
if (h->elem[*addr] == NULLKEY || *addr == Hash(key))
//循环回到原点或者该位置为无关键字
return unsuccess;
}
return success;
}
五、散列表查找性能分析
散列查找的平均查找长度取决于三个因素:
-
散列函数是否均匀,散列函数的好坏直接影响着出现冲突的频繁程度
-
处理冲突的方法,比如线性探测处理冲突可能会产生堆积,显然就没有二次探测法好,而链地址法处理冲突不会产生任何堆积,因而具有更佳的平均查找性能。
-
散列表的装填因子,所谓的装填因子a=填入表中的记录个数/散列表长度。a标志着散列表的装满的程度。当填入表中的记录越多,x就越大,产生冲突的可能性就越大。