哈希表
前言
在顺序表中查找时,需要从表头开始,依次遍历比较a[i]与key的值(也就是所查找的值)是否相等;在有序表中查找时,我们经常使用的是二分查找,通过比较key与a[i]的大小来折半查找。
但是,这两种方法的效率都依赖于查找中比较的次数。有人就研究想到,能不能不经过比较,而是直接通过关键字key一次得到所要的结果呢?这时,就有了散列表查找(哈希表)。
概念
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JpsnXZlp-1587510816440)(D:\数据结构作业\哈希.jpg)]
当进行数据查询时,数组可以直接通过下标迅速访问数组中的元素。 而链表则需要从第一个元素开始一直找到需要的元素位置,显然,数组的查询效率会比链表的高。当进行增加或删除元素时,在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样,如果想删除一个元素,需要移动大量去填掉被移动的元素,而链表只需改动元素中的指针即可实现增加或删除元素
那么哈希表,是既能具备数组的快速查询的优点,又能融合链表方便快捷的增加删除元素的优势。哈希表就是链表数组的结合体。
程序
#define M 997 //哈希表的长度
struct HashTable
{
int key; //关键字
... //其他数据域
}
冲突
哈希表还需要解决的问题就是冲突,两个不同的关键字通过哈希函数计算得出相同的哈希数值,因而被映射到表上的同一个位置上的现象称为冲突,冲突不可避免,只能减少。怎样解决冲突下面会给相应的解答。
哈希函数构造方法
-
直接定值法
取关键字或者是关键字的某个线性函数为哈希地址 H(key)=key or H(key)=a*key+b.(其中为a、b常数)
这样的好处很明显,H(key)与key的值是一一对应的,也就是得到的值是不会冲突,但缺点也很明显,要求关键字的规律性很强。
-
数据分析法
在关键字比较长且关键字的形式已经事先知道的情况下,可以通过分析选取其中几位随机性好的关键字组合后成为哈希地址。通俗的讲就是要存很多很长的多位数,取其中的若干位数作为地址。
-
平方取中法
对关键码进行平方,按照哈希表的大小,取中间若干位作为哈希地址。也就是将关键码平方之后用数据分析法得出存储地址,存储数据。
-
折叠法
将关键字分割成位数相同的几个部分,然后取这几个部分的叠加和作为哈希地址。
-
移位折叠
去掉进位
-
边界折叠
相邻的两部分之间会有颠倒
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UuQyIfsg-1587510816446)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\1575163226944.png)]
-
-
除留余数法
它取关键字被某个不大于哈希表表长m的数p除后所得的余数为哈希地址,p有限制必须是质数,即
H(key)=key%p
-
乘余取整法
哈希函数由自己构造的这几个只是一些常用的哈希函数。
冲突的解决方法
处理冲突是指对于一个待插入哈希表的数据元素,若按给定的哈希函数所求得的哈希地址已被占用,则按一定的规则求下一哈希地址,如此重复,直到找到一个可用的地址存储该数据元素。
通常解决冲突的方法有两类,开放地址法和链地址法
-
开放地址法
**Hi=[H(key)+di]%m , i=1、2、3… ** 其中m为哈希表长 ,H(key)为哈希函数,di为增量
若di=1、2、3…则称为线性探测再散列
利用线性探测解决冲突的构造算法
//哈希表为H[]、m为哈希表长,eptr是要存入哈希表的数据地址,
int LinearCreat(datatype H[],int m, datdtype *eptr)
{
int d;
for(i=0;i<m;i++)
{
H[i]=0;//哈希表初始化
}
while(eptr->key!=0)
{
d=Hash(eptr->key);
while(H[d]!=0)
{
d=(d+1)%m;
H[d]=*eptr
eptr++;
}
}
}
利用线性探测法解决冲突的查找算法
//在哈希表H[M]中插入新的节点new
void LinearInsert(datype H[],int m,keytype key)
{//哈希表H,m为哈希表长,key为所查找的关键字
int d,begin;
begin=d=Hash(key);
while(H[d]!=0&&H[key]!=key) //当查找哈希表中的关键字不是key
{
d=(d+1)%m; //利用解决冲突的方法再计算地址
if(d==begin)
{
d=-1;
berak;
}
}
if(H[d]==0)
{
d=-1;
}
return d;//返回地址-1代表查找失败
}
若di=12、-12、22、-22、…则称为二次线性再散列
线性再散列遇到堆积问题(很多元素在相邻的哈希地址上堆积起来)一般用二次线性再散列。
双哈希函数的构造
双散列是以关键字的另一个关键字的散列值作为增量。设有两个哈希函数H1和H2,则选择的D(i)=i*H2(key).即当地址i放生冲突时,探测的是i+H2(key)、i+H2(key)、i+3H2(key)…的地址。
-
链地址法
链地址法就是将哈希表的每一个空间定义为一个单链表的表头指针,这个单链表的表头指针包含一个数据域和一个指针域,其中指针域存储关键字,当发生冲突时,将所有哈希值相同的关键字链接在以该哈希地址为表头指针的链表中。
typedef struct hnode {datatype data; /*关键字*/ … /*其它信息*/ struct hnode *next; /*指向下一个同义词的指针*/ }HNode; 定义散列表(指针向量): #define m … /*m为散列表的容量*/ HNode *HashTbl[m];
//以拉链法处理冲突构造的散列表 void CreateLHTbl(HNode *LHashTbl[m], datatype *eptr) {/*用拉链法处理冲突建立散列表LHashTbl*/ /*eptr为待放入散列表中的数据基址,Hash ()为散列函数*/ int d; HNode *q; for(i=0; i<m; i++) LHashTbl[i]=NULL; /*散列表初始化*/ while(eptr->data.key!=0) { /*待放入散列表的元素,其关键码0表示结束*/ d= Hash (eptr->data.key); /*求 当前元素的散列地址*/ q= (HNode *) malloc(sizeof(HNode)) ; /*申请新的一个结点空间*/ q->data.key=eptr->data.key;/*填装结点信息*/ q->next=LHashTb1[d];/*插入到同义词的链表*/ LHashTbl[d]=q; eptr++;/*指向下一个元素*/ } }
//以拉链法处理冲突构造的散列表中的查找 HNode *SearchLHTb1(HNode *LHashTb1[m],keytype key) {/*LHashTbl 是用拉链法处理冲突建立的散列表,散列函数为Hash(),m为散 列表的长度,查找关键码为key的元素,成功返回其地址,否则返回NULL*/ int d; HNode *q; d=Hash(key); /*求 当前元素的散列地址*/ q=LhashTbl[d]; /*同义词链表的头指针*/ while (q) { if (q->data. key==key) break; else q=q->next; } return q; }
哈希表的查找分析
为了讨论哈希表的平均查找长度,需要引出一个概念装填因子α,α=表中的记录数/哈希表的长度,如果α越小说明表还有许多空单元,发生冲突的可能性就越小,如果α越大说明表越满,则发生冲突的可能性越大。
已有人证明线性探测法的哈希表的查找成功的平均长度为S≈1/2(1+1/(1-α))。
采用链地址法的哈希表的查找成功的平均查找长度为S≈1+α/2。