关键词:Hash表,Hash函数,冲突解决。
一、哈希表原理及运用
1、定义:散列表(Hash table),即哈希表,是根据关键字(Key value)而直接访问在内存存储位置的数据结构。也就是说,它是通过计算一个关于键值的函数,将所需查询的数据映射到表中的一个位置来访问记录,因此这可以加快查找速度。这个映射函数被称作散列函数,存放记录的数组被称作散列表。
2、思想:Hash算法的主要思想就是建立一种键-值(key-index)的储存数据的结构。我们只需要通过Hash函数将输入的待查找的值key转化成一个对应的整数,那么就可以将这个整数即键,作为一个简单的无序数组的索引,于是便可以快速的访问任意键的值。
3、运用:Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值,从而达到加密的作用;日常生活中网络上常见歌曲电影等文件都有一个对应的Hash值,通过该值便可以做到快速访问的效果。
二、哈希函数的解析
在本次实验中,我一共用到了三种哈希函数,即BKDR、DJB和ELF,三种散列函数的实现方法如下:
BKDR散列函数
unsigned int BKDR_hash(string s) {
unsigned int seed = 131;
unsigned long hash = 0;
int len = s.length();
for (int i = 0;i < len;i ++)
hash = hash * seed + s[i];
return hash & 0x7FFFFFFF;
}
DJB散列函数
unsigned DJB_hash(string s) {
unsigned long hash = 5381;
int len = s.length();
for (int i = 0;i < len;i ++)
hash += (hash << 5) + s[i];
return hash & 0x7FFFFFFF;
}
ELF散列函数
unsigned int ELF_hash(string s) {
unsigned long hash = 0;
int len = s.length();
for (int i = 0;i < len;i ++) {
hash = (hash << 4) + s[i];
unsigned long g = hash & 0xf0000000L;
if (g)
hash ^= g >> 24;
hash &= ~ g;
}
return hash;
}
由于处理的对象为string类型,那么需要对string的每一位的字符进行运算处理,才可以得到一个相对唯一的hash值。三个函数看上去差别很大,但实际上也都是通过对每一位字符进行运算,然后进行取模和移位等操作,从而得到最终的Hash值。由于Hash值的范围会很大,所以在得到Hash值之后需要对Hash表的范围进行取模,确保该Hash值正好落在Hash表的范围内。
unsigned int,无符号的整型数据类型,在32位和64位的操作系统中都是占用4个字节,每个字节对应8位,所以一共对应32位,数值范围的上限应该减去1。1个16进制的F对应2进制数为1111,因此最多可以对应7个F。hash & 0x7FFFFFFF的意思也就是将hash的值与0x7FFFFFFF作取模运算。
三、冲突解决方法
在字符串通过Hash函数计算取模后,不同的字符串可能会得到相同的Hash值,这样的话多个字符串就会插入到同一个数组位置而造成数据的丢失和不完整。因此,为了避免这种冲突,我在实验中一共用了三种解决冲突的办法,即开放定址的线性探测法、二次探测法和拉链法。
1、开放定址法
开放定址法的基本思想就是将哈希表看成是一个循环的链式结构,即数组的头和尾链接起来。
在建立哈希表的时候,如果在插入数据的时候发生了冲突,那么就根据探测的方法向前逐个单元的查找,直到能够找到一个空位,然后将数据插入;在查找的时候,如果发现该位置的数据不对的时候,就根据相应的探测方法向前逐个单元的查找,直到找到正确的数据,如果查找到数据为空的情况,则说明查找失败,哈希表中没有这个数据。
(1)、线性探测法
线性探测法在构建哈希表的时候,特别要注意每次发生冲突后需要将下标加1之后在对哈希表的长度取模,形成一个循环的结构。代码如下:
void linear(int index,string eng,string chi) {
while (1) {
index ++;
index %= test;
if (dict[index].english == "") {
dict[index].english = eng;
dict[index].chinese = chi;
dict[index].next = NULL;
break;
}
else
continue;
}
}
对于查找的时候,另外需要重新定义两个计数器,来记录在未能判断是查找成功还是查找失败的时候进行查找的次数。当查找结束后,将对应查找成功或者失败的计数器加到总的查找计数器上。代码如下:
string linear_judge(int index,string eng) {
int su = 0;
int in_su = 0;
while (1) {
index %= test;
if (dict[index].english == "") {
in_sum += in_su + 1;
in_sumword ++;
return "Not exist.";
}
else if (dict[index].english != eng) {
su ++;
in_su ++;
index ++;
index %= test;
}
else {
sum += su + 1;
sumword ++;
return dict[index].chinese;
}
}
}
(2)、二次探测法
二次探测法在构造哈希表的时候不同于线性的是每次下标加的是i的平方,而且i从1开始随着探测次数递增。代码如下:
void quadratic(int index,string eng,string chi) {
int i = 1;
while (1) {
index += i*i;
index %= test;
if (dict[index].english == "") {
dict[index].english = eng;
dict[index].chinese = chi;
dict[index].next = NULL;
break;
}
else
i ++;
}
}
对于查找的时候,与构造哈希表时改变相同的代码。代码如下:
string quadratic_judge(int index,string eng) {
int su = 0;
int in_su = 0;
int i = 0;
while (1) {
index += i*i;
index %= test;
if (dict[index].english == "") {
in_sum += in_su + 1;
in_sumword ++;
return "Not exist.";
}
else if (dict[index].english != eng) {
su ++;
in_su ++;
i ++;
continue;
}
else {
sum += su + 1;
sumword ++;
return dict[index].chinese;
}
}
}
2、拉链法
在我们写代码的时候,常用的数据结构有数组和链表。数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么结合数组和链表的优点,就得到了拉链法,结构模型如图: