小白学编程之哈希表
当我们用数组去存储数据的时候是不是会有这样的烦恼,就是你要找一个数必须一个一个遍历数组里的数,直到找到你需要找的数。这样的方式最快的情况是此数下标为0,最坏的情况是下标为n-1。在数组元素少的时候我们察觉不到,但是当数组中的元素多了的时候寻找起来就很费时间。我们需要寻找一个更加方便找到数组中元素的方法,由此引出了我们今天的主角哈希表
哈希表存储的基本思想是:以数据表中的每个记录的关键字 k为自变量,通过一种函数H(k)计算出函数值。把这个值解释为一块连续存储空间(即数组空间)的单元地址(即下标),将该记录存储到这个单元中。在此称该函数H为哈希函数或散列函数。按这种方法建立的表称为哈希表或散列表。
例
要将关键字值序列(3,15,22,24),存储到编号为0到4的表长为5的哈希表中。
计算存储地址的哈希函数可取除5的取余数算法H(k)=k % 5。则构造好的哈希表如图1所示。
图1
分析:
先将第一个关键字3通过取余数算法H(k) = k % 5,得到余数为3,那我们就将3这个关键字存放在下标为3的元素中。其他的关键字和第一个关键字的操作方法是一样的。
int function(int num){
int ret = num % 5;
return ret;
}
int main(){
int a[5] = {0}; //初始化
int num, int down;
printf("请输入关键字");
while(scanf("%d", &num) == 1){
down = function(num);
if(a[down] == 0){ //判断下标down的元素是否为0,是0的话就赋值
a[down] = num;
}
printf("请输入关键字或按q离开");
}
return 0;
}
从上述代码中我们不难发现一件事件,就是当我们有两个关键字通过取余算法得到的下标都一样的话(冲突),那一个下标怎么能存储的下两个关键字呢
通过对哈希表的分析我们必须考虑以下两个情况:
(1)如何设计哈希函数以使冲突尽可能少地发生。
(2)发生冲突后如何解决。
我们先来研究如何设计冲突少的哈希函数,构造好的哈希函数的方法,应能使冲突尽可能地少,因而应具有较好的随机性。这样可使一组关键字的散列地址均匀地分布在整个地址空间。根据关键字的结构和分布的不同,可构造出许多不同的哈希函数。
1、直接定址法
2、除留余数法
3、平方取中法
4、折叠法
5、数值分析法
以上5种方法都是可以减少冲突的函数,由于篇幅有限咱们只挑比较常用的方法2进行分析,其余的方法如果大家有兴趣可以自己去找答案。
除留余数法
取关键字k除以哈希表长度m所得余数作为哈希函数地址的方法。即:
H(k)=k%m
这是一种较简单、也是较常见的构造方法。这种方法的关键是选择好哈希表的长度m。使得数据集合中的每一个关键字通过该函数转化后映射到哈希表的任意地址上的概率相等。理论研究表明,在m取值为素数(质数)时,冲突可能性相对较少。
当我们设计好了函数之后,还是有可能会出现冲突的情况,这时候就需要我们解决冲突。
(1)开放地址法
(2)链地址法
1.开放定址法
用开放定址法处理冲突就是当冲突发生时,形成一个地址序列。沿着这个序列逐个探测,直到找出一个“空”的开放地址。将发生冲突的关键字值存放到该地址中去。
如 Hi=(H(k)+d(i)) % m, i=1,2,…k (k<m-1)
其中H(k)为哈希函数,m为哈希表长,d为增量函数,d(i)=dl,d2…dn-l。
增量序列的取法不同,可得到不同的开放地址处理冲突探测方法。
(1) 线性探测法
线性探测法是从发生冲突的地址(设为d)开始,依次探查d+l,d+2,…m-1(当达到表尾m-1时,又从0开始探查)等地址,直到找到一个空闲位置来存放冲突处的关键字。
若整个地址都找遍仍无空地址,则产生溢出。
线性探查法的数学递推描述公式为:
d0=H(k)
di=(di-1+1)% m (1≤i≤m-1)
(2) 平方探查法
设发生冲突的地址为d,则平方探查法的探查序列为:d+12,d+22,…直到找到一个空闲位置为止。
平方探查法的数学描述公式为:
d0=H(k)
di=(d0+i2) % m (1≤i≤m-1)
2.链地址法
用链地址法解决冲突的方法是:把所有关键字为同义词的记录存储在一个线性链表中,这个链表称为同义词链表。并将这些链表的表头指针放在数组中(下标从0到m-1)。这类似于图2中的邻接表和树中孩子链表的结构。
图2
例:
/*定义结构体*/
struct node{
int data;
struct node* next;
}
/*初始化定义的结构体数组*/
void init_hash(struct node* ps[]){
for(int i = 0; i < LEN; i++){
ps[i] = NULL;
}
}
/*赋值*/
void hash_fun(struct node* ps[], int key){
int index = key % LEN;
struct node* pnew = (struct node*)molloc(sizeof(struct node));
assert(pnew != NULL);
pnew -> data = key;
pnew -> next = NULL;
if(ps[index] -> next == NULL){
ps[index] -> next = pnew;
}
else{
struct node* p = ps[index] -> next;
while(p -> next != NULL){
p = p -> next;
}
p -> next = pnew;
}
return ;
}
由于在各链表中的第一个元素的查找长度为l,第二个元素的查找长度为2,依此类推。因此,在等概率情况下成功的平均查找长度为:
(15+22+3l+41)/9=16/9
虽然链地址法要多费一些存储空间,但是彻底解决了“堆积”问题,大大提高了查找效率。