哈希表(散列)
定义
- 哈希表
哈希表(散列表)是一种将关键字key与地址或其它数据建立起直接映射关系的数据结构。哈希表通过哈希函数(hash函数)将关键字key进行运算并映射到散列表中的某一位置,建立起这样的映射关系可以加快对数据的查找和访问。 - 哈希函数
哈希函数的功能是建立其关键字key与哈希表中存储位置的映射关系。
将关键字按一定算法转换成数值,再对数值做一定处理得出hashcode,hashcode代表了数据对象在哈希表中存储的索引位置index。
一个好的hash函数应具备两个条件:尽可能地将数据元素均匀地分布在hash表中,这样做的好处是可以减少hash冲突;hash函数对关键字的映射运算的时间复杂度应是常数,不能太大。 - 哈希冲突
由于hash函数对不同关键字的映射结果可能是相同的,也就是说,不同的key,经过hash(key)后得出的hashcode可能相同,这样一来两个不同的数据对象就映射到了hash表中的同一存储位置,这样的现象叫做hash冲突。
解决hash冲突的方式
1.链式分离法
该方法的主要思想是建立链表来解决hash冲突,将在同一位置产生hash冲突的元素链接起来组成链表,再将链表插入到hash表中的对应位置。这样一来当我们需要访问查找一个关键字对应的数据时,就需要先通过hash函数获得其索引位置,然后遍历该索引位置对应的链表,逐个节点对关键字key进行比较最终完成查找。
链式分离法的优缺点:
这里引用一下别的博主的文章:
https://blog.csdn.net/ldw662523/article/details/79567817
- 链地址法的优点
与开放定址法相比,拉链法有如下几个优点:
①链地址法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于链地址法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而链地址法中可取α≥1,且结点较大时,链地址法中增加的指针域可忽略不计,因此节省空间;
④在用链地址法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。 - 链地址法的缺点
链地址法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
链式分离法的C语言代码实现:
#include <stdio.h>
#include <stdlib.h>
//定义数据对象
typedef struct stu
{
char name[50];
int score;
} stu;
//定义数据表
typedef struct stu_list
{
struct stu * stus;
int size;
}stu_list;
//定义链表(解决哈希冲突)
typedef struct linknode
{
char name[50];
int score;
struct linknode * next;
} linknode,*linklist;
//定义哈希表
typedef struct HashTable
{
linknode ** elements;
int table_size;
} HashTable;
linklist linklist_add(linklist l,stu s)
{
if(l==NULL)
{
linklist head=(linklist)malloc(sizeof(linknode));
head->next=(linklist)malloc(sizeof(linknode));
strcpy(head->next->name,s.name);
head->next->score=s.score;
head->next->next=NULL;
return head;
}
linknode * p=l,*q;
while(p->next!=NULL)
{
p=p->next;
}
q=(linklist)malloc(sizeof(linknode));
strcpy(q->name,s.name);
q->score=s.score;
q->next=NULL;
return l;
}
//初始化哈希表
HashTable *Init_HashTable(int table_size)
{
HashTable * HT=(HashTable *)malloc(sizeof(HashTable));
HT->table_size=table_size;
HT->elements=(linknode **)calloc(table_size,sizeof(linknode *));
return HT;
}
//创建数据表
stu_list* Create_stulist(int size)
{
if(size<0)
{
printf("表的大小非法!");
getchar();
return;
}
printf("\n创建数据表:\n");
int i,j,k;
stu_list * sl=(stu_list*)malloc(sizeof(stu_list));
sl->size=size;
sl->stus=(stu_list *)calloc(size,sizeof(stu));
for(i=0;i<size;i++)
{
printf("请输入学生姓名:\n");
scanf("%s",sl->stus[i].name);
printf("请输入学生成绩:\n") ;
scanf("%d",&sl->stus[i].score);
}
return sl;
}
//定义hash函数
int hash(char * key,int key_size,int table_size)
{
int i,j,k,hashcode=0;
for(i=0;i<key_size;i++)
{
hashcode = hashcode *37 +(int) key[i];
}
hashcode %= table_size;
if(hashcode<0) hashcode += table_size;
return hashcode;
}
//创建哈希表
HashTable *CreateHashTable(stu_list * sl,int table_size)
{
int i,j,k;
int index=0,len=0;
linklist l;
stu s;
HashTable *ht=Init_HashTable(table_size);
for(i=0;i<sl->size;i++)
{
s=sl->stus[i];
len=strlen(s.name);
index=hash(s.name,len,table_size);
l=ht->elements[index];
l=linklist_add(l,s);
ht->elements[index]=l;
}
return ht;
}
//根据关键字在哈希表中查找数据
int get_socre_bykey(char * key,int len,int table_len,HashTable *ht)
{
int index=hash(key,len,table_len);
linklist p=ht->elements[index];
if(p==NULL)
{
printf("NULL"); return;
}
p=p->next;
while(p!=NULL)
{
if(strcmp(p->name,key)==0)
{
return p->score;
}
p=p->next;
}
printf("\n查找失败,未查找到相关内容!");
return -1;
}
main()
{
int hashcode;
int n,i;
int table_size=5;
stu_list * sl=Create_stulist(3);
HashTable * ht=CreateHashTable(sl,table_size);
char name[50];
printf("\n请输入要查找的内容:");
scanf("%s",name);
int socre=get_socre_bykey(name,strlen(name),table_size,ht);
if(socre!=-1)
printf("\nsocre=%d",socre);
}
2.开放定址法
1.线性探测法
假设hash表足够大时,当元素间产生了hash冲突,那么可以通过再探测的方式,在表中探测出一个空的位置,将元素存入。而用于探测的函数是线性的探测方式则为线性探测法,即第i次探测的下一个位置可以表示为:index+=f(i),其中f是关于i的线性函数。
线性探测法的优点是,可以将表中的所有位置都探测到,对表内空间的利用率高,但是缺点同样非常明显,即容易产生聚集,造成表内局部的位置大量的产生冲突而造成多次的探测,降低了查找和插入的效率。
线性探测法的c语言代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义数据表和hash表
typedef struct stu
{
char name[50];
int score;
}stu;
typedef struct hashtable
{
int tablesize;
int currentsize;
stu * elements;
int * flag;
}hashtable;
//hash函数
int hash(char * key,int len,int tablesize)
{
int i,j,k;
int hashcode=0;
for(i=0;i<len;i++)
{
hashcode+=(int)key[i];
}
hashcode %= tablesize;
if(hashcode<0) hashcode+=tablesize;
return hashcode;
}
//初始化hash表
hashtable * Init_hashtable(int size)
{
if(size<=0)
{
printf("表长非法!\n");
return NULL;
}
hashtable * ht=(hashtable *)calloc(1,sizeof(hashtable));
ht->tablesize=size;
ht->currentsize=0;
ht->elements=(stu *)calloc(size,sizeof(stu));
ht->flag=(int *)calloc(size,sizeof(int));
return ht;
}
//向hash表中存入数据 (线性探测法)
int put(stu s,hashtable * ht)
{
if(ht==NULL)
{
printf("hash表不存在!\n");
return -1;
}
if(ht->currentsize>=ht->tablesize)
{
printf("hash表已满!\n");
return -1;
}
int hashcode=hash(s.name,strlen(s.name),ht->tablesize);
if(ht->flag[hashcode]==0)
{
ht->elements[hashcode]=s;
ht->flag[hashcode]=1;
return 1;
}
else
{
while(ht->flag[++hashcode]!=0);
ht->elements[hashcode]=s;
ht->flag[hashcode]=1;
return 1;
}
}
//从hash表中查找获取元素
int get(char * key,int len,hashtable *ht)
{
if(ht==NULL)
{
printf("hash表不存在!\n");
return -1;
}
int hashcode=hash(key,strlen(key),ht->tablesize);
while(ht->flag[hashcode]!=0)
{
if(strcmp(key,ht->elements[hashcode].name)==0)
{
return ht->elements[hashcode].score;
}
hashcode++;
}
return -1;
}
main()
{
int tablesize=13;
hashtable *ht=Init_hashtable(tablesize);
stu s1={"xiaohua",88};
stu s2={"xiaogou",59};
stu s3={"goudan",99};
put(s1,ht);
put(s2,ht);
put(s3,ht);
int score = get(s2.name,strlen(s2.name),ht);
if(score!=-1)
{
printf("查找结果为:%d\n",score);
}
else
{
printf("未查找到相关内容!\n");
}
}