数据结构——全面学习哈希
2015年08月18日 11:42:09
阅读数:1125
相信学计算机的童鞋对于“哈希”这个词会很熟悉,但是能明明白白的说清楚,并且用程序来描述的人还是比较少的。这里,我们就全面学习这个重要的数据结构,以及它的思想和应用。
首先,我们来学习一下几个基本概念。
哈希(hash)
是一种数据编码方式。将大尺寸的数据(如一句话,一张图片,一段音乐、一个视频等)浓缩到一个数字中,从而方便地实现数据匹配和查找的功能。
哈希表(hash table)
一种数据表结构。hash表,有时候也被称为散列表。个人认为,hash表是介于链表和二叉树之间的一种中间结构。链表使用十分方便,但是数据查找十分麻烦;二叉树中的数据严格有序,但是这是以多一个指针作为代价的结果。hash表既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。
打个比方来说,所有的数据就好像许许多多的书本。如果这些书本是一本一本堆起来的,就好像链表或者线性表一样,整个数据会显得非常的无序和凌乱,在你找到自己需要的书之前,你要经历许多的查询过程;而如果你对所有的书本进行编号,并且把这些书本按次序进行排列的话,那么如果你要寻找的书本编号是n,那么经过二分查找,你很快就会找到自己需要的书本;但是如果你每一个种类的书本都不是很多,那么你就可以对这些书本进行归类,哪些是文学类,哪些是艺术类,哪些是工科的,哪些是理科的,你只要对这些书本进行简单的归类,那么寻找一本书也会变得非常简单,比如说如果你要找的书是计算机方面的书,那么你就会到工科一类当中去寻找,这样查找起来也会显得麻烦。
映像
由哈希函数得到的哈希表是一个映像。
冲突
如果两个关键字的哈希函数值相等,这种现象称为冲突。
哈希算法
并不是一个特定的算法而是一类算法的统称。 哈希算法也叫散列算法,一般来说满足这样的关系:f(data)=key,输入任意长度的data数据,经过哈希算法处理后输出一个定长的数据key。同时这个过程是不可逆的,无法由key逆推出data。
处理冲突的几个方法(后面的程序会详解)
- 开放地址法:用开放地址处理冲突就是当冲突发生时,形成一个地址序列,沿着这个序列逐个深测,直到找到一个“空”的开放地址,将发生冲突的关键字值存放到该地址中去。 例如:hash(i)=(hash(key)+d(i)) MOD m (i=1,2,3,......,k(k<m-1)) d为增量函数,d(i)=d1,d2,d3,...,dn-1 根据增量序列的取法不同,可以得到不同的开放地址处理冲突探测方法。 有线性探测法、二次方探测法、伪随机探测法。
- 链地址法:把所有关键字为同义词的记录存储在一个线性链表中,这个链表成为同义词链表,即把具有相同哈希地址的关键字值存放在同义链表中。
- 二次哈希表:费时间的一种方法 如果是一个data数据集,经过哈希算法处理后得到key的数据集,然后将keys与原始数据进行一一映射就得到了一个哈希表。一般来说哈希表M符合M[key]=data这种形式。 哈希表的好处是当原始数据较大时,我们可以用哈希算法处理得到定长的哈希值key,那么这个key相对原始数据要小得多。我们就可以用这个较小的数据集来做索引,达到快速查找的目的。
稍微想一下就可以发现,既然输入数据不定长,而输出的哈希值却是固定长度的,这意味着哈希值是一个有限集合,而输入数据则可以是无穷多个。那么建立一对一关系明显是不现实的。所以"碰撞"(不同的输入数据对应了相同的哈希值)是必然会发生的,所以一个成熟的哈希算法会有较好的抗冲突性。同时在实现哈希表的结构时也要考虑到哈希冲突的问题。
密码上常用的MD5,SHA都是哈希算法,因为key的长度(相对大家的密码来说)较大所以碰撞空间较大,有比较好的抗碰撞性,所以常常用作密码校验。
下面,我们通过一个例子,来自己实现一个完整的哈希表
问题描述:针对某个集体(比如你所在的班级)中的“人名”设计一个哈希表,使得平均查找长度不超过R,完成相应的建表和查表程序。
思路:假设人名为中国人姓名的汉语拼音形式。待填入哈希表的人名共有30个,取平均查找长度的上限为2。哈希函数用除留余数法构造,用伪随机探测再散列法处理冲突。
代码如下 :
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <string>
-
#include <iostream>
-
#ifndef _HashTest_H_
-
#define _HashTest_H_
-
#define NAME_NO 30
-
#define HASH_LENGTH 50
-
#define M 50;
-
using namespace std;
-
typedef struct
-
{
-
char *py; //名字的拼音
-
int k; //拼音所对应的整数
-
}NAME;
-
typedef struct //哈希表
-
{
-
char *py; //名字的拼音
-
int k; //拼音所对应的整数
-
int si; //查找长度
-
}HASH;
-
#endif
-
HASH HashList[HASH_LENGTH],NameList[HASH_LENGTH];
-
void InitNameList()
-
{
-
char *f;
-
int r,s0,i;
-
NameList[0].py="zhoujielun";
-
NameList[1].py="naying";
-
NameList[2].py="halin";
-
NameList[3].py="wangfeng";
-
NameList[4].py="fenghanzao";
-
NameList[5].py="fuzongkai";
-
NameList[6].py="hujingbin";
-
NameList[7].py="huangjianwu";
-
NameList[8].py="lailaifa";
-
NameList[9].py="lijiahao";
-
NameList[10].py="liangxiaocong";
-
NameList[11].py="linchunhua";
-
NameList[12].py="liujianhui";
-
NameList[13].py="luzhijian";
-
NameList[14].py="luonan";
-
NameList[15].py="quegaoxiang";
-
NameList[16].py="sugan";
-
NameList[17].py="suzhiqiang";
-
NameList[18].py="taojiayang";
-
NameList[19].py="wujiawen";
-
NameList[20].py="xiaozhuomin";
-
NameList[21].py="xujinfeng";
-
NameList[22].py="yanghaichun";
-
NameList[23].py="yeweixiong";
-
NameList[24].py="zengwei";
-
NameList[25].py="zhengyongbin";
-
NameList[26].py="zhongminghua";
-
NameList[27].py="chenliyan";
-
NameList[28].py="liuxiaohui";
-
NameList[29].py="panjinmei";
-
for(i=0;i<NAME_NO;i++)
-
{
-
s0=0;
-
f=NameList[i].py;
-
for(r=0;*(f+r)!='\0';r++)/*将字符串的各个字符所对应的ASCII码相加,所得的整数做为哈希表的关键字*/
-
s0=*(f+r)+s0;
-
NameList[i].k=s0;
-
}
-
}
-
void CreateHashList()
-
{
-
int i;
-
for(i=0; i<HASH_LENGTH;i++)
-
{
-
HashList[i].py="";
-
HashList[i].k=0;
-
HashList[i].si=0;
-
}
-
for(i=0;i<NAME_NO;i++)
-
{
-
int sum=0;
-
int adr=(NameList[i].k)%M;//哈希函数
-
int d=adr;
-
if(HashList[adr].si==0)//如果不冲突
-
{
-
HashList[adr].k=NameList[i].k;
-
HashList[adr].py=NameList[i].py;
-
HashList[adr].si=1;
-
}
-
else//冲突
-
{
-
do
-
{
-
d=(d+NameList[i].k%10+1)%M;//伪随机探测再散列法处理冲突
-
sum=sum+1; //查找次数加1
-
}while (HashList[d].k!=0); //找到一个空位置
-
HashList[d].k=NameList[i].k;
-
HashList[d].py=NameList[i].py;
-
HashList[d].si=sum+1;
-
}
-
}
-
}
-
void FindList() //查找
-
{
-
char name[20]={0};
-
int s0=0,r,sum=1,adr,d;
-
printf("input name:");
-
scanf("%s",name);
-
for(r=0;r<20;r++) //求出姓名的拼音所对应的整数(关键字)
-
s0+=name[r];
-
adr=s0%M; //使用哈希函数
-
d=adr;
-
if(HashList[adr].k==s0) //分3种情况进行判断
-
printf("\nname:%s key:%d find times: 1",HashList[d].py,s0);
-
else if (HashList[adr].k==0)
-
printf("no records!");
-
else
-
{
-
int g=0;
-
do
-
{
-
d=(d+s0%10+1)%M; //伪随机探测再散列法处理冲突
-
sum=sum+1;
-
if(HashList[d].k==0)
-
{
-
printf("no records! ");
-
g=1;
-
}
-
if(HashList[d].k==s0)
-
{
-
printf("\nname:%s key:%d find times:%d",HashList[d].py,s0,sum);
-
g=1;
-
}
-
}while(g==0);
-
}
-
}
-
void Display()
-
{
-
int i;
-
float average=0;
-
printf("\naddress\tkey\t\ttimes\tH(key)\t name\n"); //显示的格式
-
for(i=0; i<50; i++)
-
{
-
printf("%d ",i);
-
printf("\t%d ",HashList[i].k);
-
printf("\t\t%d ",HashList[i].si);
-
printf("\t\t%d ",HashList[i].k%30);
-
printf("\t %s ",HashList[i].py);
-
printf("\n");
-
}
-
for(i=0;i<HASH_LENGTH;i++)
-
average+=HashList[i].si;
-
average/=NAME_NO;
-
}
-
int main()
-
{
-
char ch1;
-
InitNameList();
-
CreateHashList ();
-
do
-
{
-
printf("D. show hashTable\nF. find\nQ. quit\nplease choose");
-
cin>>&ch1;
-
switch(ch1)
-
{
-
case 'D':Display(); cout<<endl;break;
-
case 'F':FindList(); cout<<endl;break;
-
case 'Q':exit(0);
-
}
-
cout<<"come on !(y/n):";
-
cin>>&ch1;
-
}while(ch1!='n');
-
return 0;
-
}
我们执行这段程序,会出现如下结果:
根据输出我们可以看到四个值:
- 地址
- 关键字
- 搜索长度 H(key)
- 姓名
关键字就是拼音的ascll码之和。然后通过哈希函数形成了一个值,我们叫做value这段程序是用50取余数之后得到的,所以key值在0~50之间。这个value值决定了数据存放的位置,所以能看到value值跟地址很多是一样的。这里是如何解决“冲突”的呢。如果有两个元素的经过哈希函数之后的结果是一样的呢?这里定义了一个si的值,意思为搜索次数,如果没有冲突意思是一次性找到了位置。
我们来看这一段函数
-
else //冲突,d的初始值为adr,adr意思是address,也是value值赋予的
-
{
-
do
-
{
-
d=(d+NameList[i].k%10+1)%M;
-
sum=sum+1;
-
}while(HashList[d].k!=0); //找到空位
-
HashList[d].k=NameList[i].k; //放入元素
-
HashList[d].py=NameList[i].py;
-
HashList[d].si=sum+1;
-
}
其中
d=(d+NameList[i].k%10+1)%M
叫做伪随机探测再散列法处理冲突,就是将散列值(value),d,重新散列一次重新获得一次d的值找到一个空的位置,把元素塞进去。
再看一看key是如何得到的,也就是姓名的ascll码之和是如何得到的。
-
for(i=0;i<NAME_NO;i++)
-
{
-
S0=0;
-
f=NameList[i].py;
-
for(r=0;*(f+r)!='\0';r++)
-
s0=*(f+r)+s0;
-
NameList[i].k=s0;
-
}
这里注意一下,f是字符类型,r是int型,有一个类型转换在这里。