散列表查找(哈希算法)的定义与实现

197 阅读5分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

散列表查找(哈希算法)的定义与实现

1 散列表查找定义

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系ff,使得每个关键字key对应一个存储位置f(key)f(key)

存储位置=f(关键字)

对应关系ff称为散列函数,又称为哈希(Hash)函数。

采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。散列技术既是一种存储方法,也是一种查找方法。

散列函数可能会把两个或两个以上的不同关键字映射到同一地址,称这些情况为冲突,这些发生碰撞的不同关键字称为同义词。

2 散列函数的构造方法

3.1 直接定址法

对于下图所示的0-100岁的人口数字统计表,对年龄这个关键字就可以直接用年龄的数字作为地址,此时f(key)=key

image-20220913173500287.png

如果我们要统计的是1980年后出生年份的人口数,如下图所示,我们对出生年份这个关键字可以用年份减去1980来作为地址。此时f(key)=key-1980

直接去关键字的某个线性函数值为散列地址,散列函数为:

f(key)=a×key+b(ab为常数)f(key)=a\times key+b(a、b为常数)

直接定址法的散列函数的优点是简单、均匀,也不会产生冲突,但是需要提前知道关键字的分布情况,适合查找表较小且连续的情况。

3.2 除留余数法

除留余数法是最常用的构造散列函数的方法,假设散列表表长为m,取一个不大于m但最接近或等于m的质数p,散列函数为:

f(key)=keymodp(pm)f(key)=key \quad mod\quad p(p\leq m)

假设我们有12个记录的关键字构造散列表时,就用了f(key)=keymod12f(key)=key \quad mod \quad 12的方法,比如29mod12=529\quad mod \quad 12=5,所以它存储在下标为5的位置。

image-20220913183626589.png

根据经验,若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

3 处理散列冲突的方法

3.1 开放地址法

3.1.1 线性探测法

开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

公式是:

fi(key)=(f(key)+di)MODm(di=1,2,3,,m1)f_i(key)=(f(key)+d_i)\quad MOD \quad m(d_i=1,2,3,···,m-1)

一个简单的案例,我们的关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34}\{12,67,56,16,25,37,22,29,15,47,48,34\},表长为12.我们用散列函数f(key)=keymod12f(key)=key\quad mod \quad 12

当计算前5个数{12,67,56,16,25}\{12,67,56,16,25\},都是没有冲突的散列地址,直接存入,如下图所示。

image-20220913190041616.png

计算key=37时,发现f(37)=1f(37)=1,此时就与25所在的位置冲突,于是再次进行计算f(37)=(f(37)+1)mod12=2f(37)=(f(37)+1)\quad mod \quad12=2,于是将37存入下标为2的位置,如图所示。

image-20220913190527016.png

到了key=48,我们计算得到f(48)=0f(48)=0,与12所在的0位置冲突了,继续计算f(48)=(f(48)+1)mod12=1f(48)=(f(48)+1)\quad mod\quad 12=1,与25所在的位置冲突,于是一直到f(48=(f(48)+6))mod12=6f(48=(f(48)+6))\mod 12 =6,才有空位,如图所示。

image-20220913190929365.png

这种解决冲突的开放定址法称为线性探测法。

3.1.2 二次探测

di=02,12,(1)2,22,(2)2K2,(k)2d_i=0^2,1^2,(-1)^2,2^2,(-2)^2····K^2,(-k)^2时,称为二次探测法。增加平方运算的目的是为了不让关键字都聚集在某一块区域。

公式如下:

fi(key)=(f(key)+di)MODm(di=12,(1)2,22,(2)2K2,(k)2,km/2)f_i(key)=(f(key)+d_i)\quad MOD m (d_i=1^2,(-1)^2,2^2,(-2)^2····K^2,(-k)^2,k \leq m/2)

3.1.3 伪随机探测

在冲突时,对于位移量 did_i采用随机函数计算得到,我们称为随机探测法。

fi(key)=(f(key)+di)modm(di是一个随机数列)f_i(key)=(f(key)+d_i)\quad mod \quad m(d_i是一个随机数列)

3.2 链址法

为了避免非同义词发生冲突,把所有的同义词存储在一个线性链表,如图所示。

image-20220913192319541.png

链址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。

4 散列表的查找

散列表查找取决于三个因素:散列函数、处理冲突的方法和装填因子。

装填因子α=\alpha=表中记录数n/n/散列表长度mm

散列表的平均查找长度依赖于散列表的装填因子α\alpha,不直接依赖于nnmm

α\alpha越大,表示装填的越满,发生冲突的可能性就越大。

4.1 散列表查找算法实现

散列表的结构定义如下:

#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 /* 定义散列表长为数组的长度 */
#define NULLKEY -32768 

typedef int Status;	/* Status是函数的类型,其值是函数结果状态代码,如OK等 */ 

typedef struct
{
   int *elem; /* 数据元素存储基址,动态分配数组 */
   int count; /*  当前数据元素个数 */
}HashTable;

int m=0; /* 散列表表长,全局变量 */

散列表初始化:

/* 初始化散列表 */
Status InitHashTable(HashTable *H)
{
	int i;
	m=HASHSIZE;
	H->count=m;
	H->elem=(int *)malloc(m*sizeof(int));
	for(i=0;i<m;i++)
		H->elem[i]=NULLKEY; 
	return OK;
}

除留余数法作为散列函数:

/* 散列函数 */
int Hash(int key)
{
	return key % m; /* 除留余数法 */
}

散列表插入算法:

/* 插入关键字进散列表 */
void InsertHash(HashTable *H,int key)
{
	int addr = Hash(key); /* 求散列地址 */
	while (H->elem[addr] != NULLKEY) /* 如果不为空,则冲突 */
	{
		addr = (addr+1) % m; /* 开放定址法的线性探测 */
	}
	H->elem[addr] = key; /* 直到有空位后插入关键字 */
}

代码中插入关键字时,首先算出散列地址,如果当前地址不为空关键字,则说明有冲突。此时我们应用开放定址法的线性探测进行重新寻址,此处也可更改为链地址法等其他解决冲突的办法。

散列表查找算法:

/* 散列表查找关键字 */
Status SearchHash(HashTable H,int key,int *addr)
{
	*addr = Hash(key);  /* 求散列地址 */
	while(H.elem[*addr] != key) /* 如果不为空,则冲突 */
	{
		*addr = (*addr+1) % m; /* 开放定址法的线性探测 */
		if (H.elem[*addr] == NULLKEY || *addr == Hash(key)) /* 如果循环回到原点 */
			return UNSUCCESS;	/* 则说明关键字不存在 */
	}
	return SUCCESS;
}