数据结构与算法(6)哈希表

哈希表(Hash Table),又称散列表,它是可以根据关键字的值直接进行查询和访问的数据结构。我们通常通过映射函数()将关键字映射到存储地址,建立关键字和存储地址之间的直接映射关系。这里的存储地址可以是数组下标、索引、内存地址等。

目录

一.简单的哈希思想

二.设计哈希函数

1.直接定址法

2.除留余数法

3.随机数法

4.数字分析法

5.平方取中法

6.折叠法

7.基数转换法

8.全域哈希法

对8种方法进行总结:

三.处理冲突的方法

1.开放地址法

(1)线性探测法

(2)二次探测法

(3)随机探测法

(4)再哈希法

2.链地址法

3.建立公共溢出区

一.简单的哈希思想

eg:数组a当中有n个整数,从中寻找整数key,确定key是否在其中。

最朴素的方法是通过for循环遍历,将key和每个整数一一比对,代码如下:(查n次时,时间复杂度O(n^{2}))

int find_key(int a[],int n,int key){
	int i;
	for(int i=0;i<n;i++){
		if(key==a[i]){
			return 1;
		}
		return 0;
	} 
}

高级一些的方法中,有二分查找:(查n次时,时间复杂度O(nlogn))

int check(int a[],int n,int key){
	int l=1,r=n;
	int mid=l+(r-l)/2;
	while(l<r){
		if(a[mid]>=key){
			r=mid;
		}
		else l=mid+1;
	}
	if(a[l]==key){
		return 1;
	}
	return -1;
}

我们也可以用最简单的哈希思想:通过数组下标来记录元素是否出现:

int hashtable(int a[],int table[],int n,int key){
	for(int i=0;i<n;i++){
		table[i]=0;//初始化,且要保证n>=a[i](对于0<=i<=n中的任意n)
	} 
	for(int i=0;i<n;i++){
		table[a[i]]++;//用table的下标来记录a[i]出现的次数
		if(!table[key]){
			return 1;//table[key]!=0,即说明存在一个a[i],使得a[i]==key 
		}
		return 0; 
	}	
}

PS:这类题最优还是二分双指针......

二.设计哈希函数

1.直接定址法

直接定址法指直接去关键字的某个线性函数作为哈希函数,形式如下:

//    hash(key)=a*key+b;(a,b为常数)

适用情况:事先知道关键字并且关键字集合不是很大、连续性好。

不适用情况:不连续、有大量空位。

2.除留余数法

除留余数法是一种最简单、常用的构造哈希函数的方法,优点在于不需要事先知道关键字的分布情况。假定哈希表表长为m,取一个表长<=m的最大素数p,则设计哈希函数为hash(key)=key%p,但是这样,缺点很明显,即发生冲突的概率很大,eg:m=10,p=7,10%7=3,3%7=3,3和10会起冲突。

3.随机数法

随机数法指将关键字随机化,然后使用除留余数法得到存储地址。哈希函数为hash(key)=rand(key)%p,其中rand()为C、C++中的随机函数,rand(n)表示求0~n-1的随机数。p的取值与除留余数法相同优缺点和除留余数法也相同。

4.数字分析法

数字分析法指根据每个数字在各个位上出现的频率,选择均匀分部的若干位作为哈希地址,适用于已知的关键字集合,可以通过观察和分析关键字集合得到哈希函数。

5.平方取中法

首先求关键字的平方,然后按照哈希表的大小,去中间的若干位作为哈希地址(求平方后截取),适用于事先不知道关键字的分布且关键字的位数不是很大的情况。eg:哈希地址是4位,计算关键字124563的哈希地址,取124563^{2}的中间三位数,15,515,940,969,即594,但这种方法也会发生冲突。

6.折叠法

折叠法指将关键字从左到右分割成位数相等的几部分,将这几部分叠加求和,取后几位作为哈希地址,适用于关键字位数很多且事先不知道关键字的分布的情况。折叠法分为移位折叠和边界折叠两种。移位折叠指将分割后的每一个部分的最低位对齐,然后相加求和;边界折叠如同折纸,将相邻部分沿边界来回折叠,然后对齐相加,但这种方法也会发生冲突。eg:key=313313875789,则可以4位一分割,使用移位折叠法:

 则可得到key的哈希地址为9909。

 如果用边界折叠,则如右图所示:

 则可得到key的哈希地址为6753。

7.基数转换法

即将一个m进制的数转换成n进制的数字,再取后x位,即可得到关键字的哈希地址。

8.全域哈希法

如果对关键字了解不多,则可以使用痊愈哈希法,即将多种备选的哈希函数放在一个集合H中,如果两个不同的关键字key1\neqkey、hash(key1)==hash(key2)的哈希函数个数最多为abs(H)/m,abs(H)为集合中哈希函数的个数,m为表长,则称H是全域的。

对8种方法进行总结:

1、4适用于了解关键字的情况,2、3、5、6、7、8适用于不了解关键字的情况。

三.处理冲突的方法

通过学习可知,二中的8种方法都有一个共同问题:会面临冲突。如果发生冲突,那就要解决冲突。解决冲突的方法有3种:开放地址法、链地址法、建立公共溢出区法。

1.开放地址法

开放地址法是线性存储空间上的解决方案,也被称为闭散列。当发生冲突时,采用冲突处理方法在线性存储空间上探测其他位置。hash'(key)=(hash(key)+d_{i})%m,其中hash(key)为原哈希函数,hash’(key)为探测函数,d_{i}为增量序列,m为表长。

根据增量序列的不同,开放地址法又分为线性探测法、二次探测法、随机探测法、再哈希法。

(1)线性探测法

线性探测法是最简单的开放地址法,线性探测的增量序列为=1,2,...,m-1。(m为哈希表长)

eg:key=(14,36,42,38,40,15,19,12,51,65,34,25),若m=15,哈希函数为hash(key)=key%13,则可以依次计算,14%13=1,36%13=10,42%13=3,38%13=12,40%13=1,则14放置在1位置,标注比较次数1;36放在10,标注比较次数1;42放在3,标注比较次数1;38放在12,标注比较次数1;40与14起冲突,则令d_{i}=1,(1+1)%15=2,将40放在2,标注比较次数2;15与40起冲突,则(2+1)%15=3,与42起冲突,则令d_{i}=2,(2+1+1)%15=4,将15放在4,标注比较次数3;19%13=6,将19放在6,标注比较次数1;12%13=12,与38起冲突,(12+1)%15=13,将12放在13,标注比较次数2;51%13=12,与38起冲突,(12+1)%15=13,与12起冲突,(12+1+1)%15=14,将51放在14,标注比较次数2;65%13=0,将65放在0,标注比较次数1;34%13=8,将34放在8,标注比较次数1;25%13=12,与38起冲突,(12+1)%15=13,与12起冲突,(12+1+1)%15=14,与51起冲突,(12+1+1+1)%15=0,与65起冲突,(12+1+1+1+1)%15=1,与14起冲突,(12+1+1+1+1+1)%15=2,与40起冲突,(12+1+1+1+1+1+1)%15=3,与42起冲突,(12+1+1+1+1+1+1+1)%15=4,与15起冲突,(12+1+1+1+1+1+1+1+1)%15=5,将25放在5,标注比较次数9。如下图所示:

Address

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

Key

65

14

40

42

15

25

19

34

36

38

12

51

Compare times

1

1

2

1

3

9

1

1

1

1

2

3

算法实现:

int H(int key){
	return key%13;//哈希函数
}

int Linedetect (int HT[],int HO,int key ,int &cnt){
	int Hi;
	for (int i=1;i<m;i++){
		cnt++;
		Hi=(H0+i)%m;//按照线性探测法计算下一个哈希地址
		if(HT[Hi]==NULLKEY){
			return Hi;//若单元Hi为空,则所查元素不存在
		}
		else if(HT[Hi]==key){
			return Hi; //若单元Hi中元素的关键字为key
		}
		return -1; 
	}
	
int SearchHash(int HT[],int key){
	//在哈希表HT中查找key,若查找成功,则返回下标,否则返回-1
	int H0=H(key);//根据哈希函数计算哈希地址
	int Hi,cnt=l;
	if(HT[H0]==NULLKEY){//若单元H0为空,则所查元素不存在
		return -1;
	}
	else if(HT [H0]==key){//若单元H0中元素的关键字为key,则查找成功
		cout<<"查找成功,比较次数:"<<cnt<<endl;
		return HO;
	}
	else{
		Hi=Linedetect(HT,H0,key,cnt);
		if(HT[Hi]==key){//若单元Hi中元素的关键字为key,则查找成功
			cout<<"查找成功,比较次数:"<<cnt<<endl;
			return Hi;
		}
		else{
			return -1;//若单元Hi为空,则所查元素不存在
		} 
	}
}

bool InsertHash(int HT[] ,int key){
	int HC[];
	int H0=H(key) ; //根据哈希函数H(key)计算哈希地址
	int Hi=-1, cnt=1;
	if(HT[H0]==NULLKEY){
		HC[H0]=1;//统计比较次数
		HT[H0]=key; //若单元H0为空,则放入
		return 1;
	}
	else{
		Hi=Linedetect(HT,H0, key , cnt) ;//线性探测
		if((Hi!=-1)&&(HT[Hi]==NULLKEY)){
			HC[Hi]=cnt;
			HT[Hi]=key;//若单元Hi为空,则放入
			return 1;
		}
	}
	return 0;
}

(2)二次探测法

二次探测法指采用前后跳跃式探测的方法,发生冲突时,向后1位探测,向前1位探测,向后2^{2}位探测,向前2^{2}位探测......用跳跃的方法减少堆积。

算法实现:

int Seconddetect(int HT[],int H0,int key,int &cnt){
	int Hi,m;
	for(int i=1;i<=m/2;i++){
		int i1=i*i;
		int i2=-i1;
		cnt++;
		Hi=(H0+i1)%m;//采用线性探测法计算下一个哈希地址Hi 
		if(HT[Hi]==NULLKEY){
			return Hi;//若单元Hi为空,则所查元素不存在 
		}
		else if(HT[Hi]==key){
			return Hi;//若单元Hi中元素的关键字为key,则返回key 
		}
		cnt++;
		Hi=(H0+i2)%m;//采用线性探测法计算下一个哈希地址Hi 
		if(Hi<0){
			Hi+=m;
		}
		if(HT[Hi]==NULLKEY){
			return Hi;//若单元Hi为空,则所查元素不存在 
		}
		else if(HT[Hi]==key){
			return Hi;//若单元Hi中元素的关键字为key,则返回key 
		}
	}
	return -1;
} 

(3)随机探测法

随机探测法采用伪随机数进行探测,利用随机化避免堆积。随机探测的增量序列为d_{i}=伪随机序列。

(4)再哈希法

再哈希法指通过哈希函数得到的地址发生冲突时,再利用第2个哈希函数进行处理,称之为双哈希法。再哈希法的增量序列为d_{i}=hash_{2} (key)。

注意:采用开放地址法处理冲突时,不能随便删除表中的元素,要删除必须做一个删除标记,标记其已被删除。

2.链地址法

链地址法又被称为拉链法,如果不同的关键字通过哈希函数映射到同一地址,而这些关键词为同义词,则将所有同义词都存储在一个线性链表中。

算法实现:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>//用来存字符串

typedef struct Node{
	char *str;//字符指针
	struct Node *next;//next指针
}Node;

typedef struct Hashtable{
	Node **data;//data数组
	int size;//数组大小
}HashTable;

Node *init_node(char *str,Node *head){
	Node *p=(Node *)malloc(sizeof(Node));//动态分配空间
	p->str=strdup(str);//先开辟新的空间,再将str的全部内容拷贝到新的存储空间中
	p->next=head;//初始化next,令它=head
	return p;
}

HashTable *init_hashtable(int n){//n个元素
	HashTable *h=(HashTable *)malloc(sizeof(HashTable));//动态分配空间
	h->size=n<<1;//为了效率,一般开大1倍
	h->data=(Node **)calloc(sizeof(Node *),h->size);/*新初始化的存储地址的数组每个位置初始化的时候存储的地址应该都为空,所以用calloc,不用malloc*/
	return h;
}

void clear_node(Node *node){
	if(node==NULL){
		return;//当前节点为空直接return掉
	}
	Node *p=node,*q;//用一个指针指向当前节点,用另一个指针指向当前节点的下一个位置
	while(p){
		q=p->next;
		free(p->str);//销毁当前节点指向的字符串
		free(p);//销毁节点的存储空间
		p=q;//让当前节点指向它下一个位置
	}
	return;
}

void clear_hashtable(HashTable *h){//传入hashtable的地址
	if(h==NULL){
		return;//地址为空直接return
	}
	for(int i=0;i<h->size;i++){
		clear_node(h->data[i]);
	}
	free(h->data);//销毁哈希表数据区
	free(h);//销毁哈希表的储存空间
	return;
}

int BKDRHash(char *str){//字符串哈希的经典函数
	int seed=31;//种子,默认值31
	int hash=0;
	for(int i=0;str[i];i++){
		hash=hash*seed+str[i];
	}
	return hash&0x7fffffff;//保证return的是正数
}

int insert(HashTable *h,char *str){
	int hash=BKDRHash(str);//得到hash值
	int ind=hash%h->size;//将哈希值转换成数组下标
	h->data[ind]=init_node(str,h->data[ind]);//将字符串插入到相关位置
	return 1;
}

int search(HashTable *h,char *str){
	int hash=BKDRHash(str);//先算出hash值
	int ind=hash%h->size;//再算坐标
	Node *p=h->data[ind];//用*p遍历h->data从ind后的每一位
	while(p&&strcmp(p->str,str)){
		p=p->next;
	}
	return p!=NULL;
}

int main(){
	int op;
	char str[100];
	HashTable *h=init_hashtable(100);//先存100位
	while(scanf("%d%s",&op,str)!=EOF){
		switch(op){
			case 0:{
				printf("insert %s to hashtable\n",str);
				insert(h,str);
				break;//0为插入
			}
			case 1:{
				printf("search %s result=%d\n",str,search(h,str));
				break;//1为检索
			}	
		}
	}
	return 0;
} 

3.建立公共溢出区

除了以上处理冲突的方法,也可以建立一个公共溢出区,当发生冲突时,将关键字放入公共溢出区。查找时,先根据待查找关键字的哈希地址在在哈希表中查找,为空则查找失败;如果不为空,且关键字不相等,则到公共溢出区中查找,如果为空则查找失败。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

华梦天下

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值