一、原理:存储位置与关键字一一映射。
散列技术是在记录的存储位置和它的关键字之间建立起一个确定的对应关系f,使每个关键字key对应一个存储位置f(key)。
f为散列函数,又称哈希函数。采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间成为散列表/哈希表。
二、散列函数构造方法:
1.直接定址法
直接用key值的某个线性函数当地址。
f(key) = a * key + b
简单,不起冲突,需要事先知道关键字分布,适合查找表较小且连续的情况,
2.数字分析法
当关键字位数较大时,可以抽取关键字中的几位,对抽取出来的数字做左移或者右移,或着前几位与后几位叠加等操作来获取新的地址。
适用于关键字位数较大,但知道关键字分布的情况
3.平方取中法
对关键字进行平方取中间3位。
适用与关键字位数不大且不知道关键字分布的情况
4.折叠法
关键字位数较多时,可以将关键字分为几部分,然后相加,再取其中几位。
适用于事先不知道关键字的分布,且关键字位数较多的情况
5.取模
最常用的构造散列函数方法
对于散列表长为m的散列函数公式:
f(key) = key mod p (p <= m)
p通常为小于或等于表长的最大质数(最好接近m)或不包含小于20质因子的合数。
6.随机数法
三、处理散列冲突的方法
1.开放定址法
发现冲突后去寻找下一个空的散列地址。只要散列表足够大,一定会找到一个合适的位置。
f(key) = (f(key) +
d
i
d_i
di MOD m (
d
i
d_i
di = 1,2,3,…,m-1)
也叫做线性探测法。
冲突也被叫做堆积。
该方法的问题:假设散列表中只剩一个空位,且恰好该空位在待插元素的前一个位置。按照上述方法我们需要循环一圈才能找到空位。
因此为了解决这个问题我们对d的取值进行了扩展。
令
d
i
d_i
di =
1
2
1^2
12,
−
1
2
-1^2
−12,
2
2
2^2
22,
−
2
2
-2^2
−22,…
q
2
q^2
q2,
−
q
2
-q^2
−q2, (q <= m/2)
这样就提供了双向寻找空位的可能。
增加平方运算的目的是为了不让关键字都聚集在某一块区域,也被叫做二次探测法。
还有一个方法,在发生冲突时,我们对 d i d_i di采用随机函数计算得到。称其为随即探测法。
这里用伪随机数,即设置的随机种子是相同的,因此可以生成不会重复的数列。
2、再散列函数法
即发生冲突时,更换散列函数。增加了计算时间。
3、链地址法
即在冲突位置设置单链表。查找时我们还需要遍历链表。
4、公共溢出区法
为冲突key值设置一个公共溢出区,按顺序排列。查找时,若散列表种数据与查找元素相等,则查找成功。若不相等,可以先去公共溢出区顺序查找。若找不到则返回查找失败。
对于冲突数据较少的表,公共溢出区的性能不错。
//定义散列表结构
typedef struct {
int* elem;
int count;
}HashTable;
int m = 0; //散列表长
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12
#define NULLKEY -32768
//初始化散列表
bool 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] = NULLKEY;
}
return SUCCESS;
}
//取模法构建哈希函数
int Hash(int key) {
return key % m;
}
//插入key值
void InsertHash(HashTable* H, int key) {
int addr = Hash(key);
while (H->elem[addr] != NULLKEY) {
addr = (addr + 1) % m; //若冲突,则寻找下一个位置
}
H->elem[addr] = key;
}
//查找
bool SearchHash(HashTable* H, int key) {
int addr = Hash(key);
while (H->elem[addr] != key) {
addr = (addr + 1) & m;
//若不相等,或者循环了hash表一圈的话返回unsuccess。
if (H->elem[addr] == NULLKEY || addr == Hash(key)) {
return UNSUCCESS;
}
}
return SUCCESS;
}
//主函数
int main() {
HashTable* H = (HashTable*)malloc(sizeof(HashTable));
int a[12] = { 12, 25, 38, 15, 16, 29, 78, 67, 56, 21, 22, 47 };
InitHashTable(H);
for (int i = 0; i < 12; ++i) {
InsertHash(H, a[i]);
}
cout << SearchHash(H, 56) << SearchHash(H, 100);
return 0;
}
四、散列表的性能分析
1、散列函数是否均匀
2、处理冲突的方法
3、散列表的装填因子
装填因子a = 填入表中的记录个数 / 散列表长度。 记录越多a越大,也就代表产生冲突的可能性越大。