数据结构与算法之哈希表

 传统的查找都是通过"比较"来实现的 .

    比如说,顺序查找是"等于"和"不等于"来比较.

    改进一些的方法,就是折半查找,二叉排序树,B-树查找,通过">","<","="来比较的.查找的效率依赖于查找过程中所进行的比较次数.这些查找方法元素在结构中的位置是随机的,存在不确定的关系.

最理想的查找的方法就是我们可以根据要查找的元素关键字key直接得到地址.

比如说,你在学校要找一个人,根据他的学号就能精准的找到他.

学号和这个人是相关联的,也就是说必须在要查找的元素与位置之间找到一个关联的地方,通过某个函数f,使得

存储位置=f{关键字}

那么我们可以通过查找关键字不需要比较就可以获得需要的记录的存储位置.这就是散列技术

散列技术:是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).查找时根据这个确定的对应关系找到给定key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上.

我们将这种对应关系成为散列函数(哈希函数).按这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表和或哈希表.

整个哈希过程分为两步:

1.构造表.

2.根据元素查找位置.

散列技术最适合的求解问题是查找与给定值相等的记录.

不适合用散列表的时候:

1.元素对应很多种情况,如名字张三会对应很多人.散列表适用于一个元素对应一种情况,如学号与学生.

2.散列表也不适用于范围查找,如查找18-22岁的学生.

另一个问题就是冲突问题.不同的关键字可能会得到 同一哈希地址,即key1 不等于key,f(key1)=f(key2),这种现象称为冲突,并把Key1和key2,称为这个散列函数的同义词.

一般情况下,冲突只能尽可能的少,而不能完全避免.因为哈希函数一个压缩映像,这就不可能避免.因此在建造哈希表的时候不仅要设定一个好的哈希函数,而且要设定一种处理冲突的方法.

哈希函数的构造:

我们说过要设定一个好的哈希函数,什么才算是一种好的哈希函数?

1.计算简单

2.散列地址均匀分布.换句话说,就是使关键字经过哈希函数得到一个随机的地址,以便使一组关键字的哈希地址均匀分布在整个地址区间中,从而减少冲突.

常见的构造哈希函数的方法有:

1.直接定址法

取关键字的某个线性函数为散列地址:Hash(key)=A*key+B;

其中A,B为常数.

优点;简单,均匀

缺点:需要事先知道关键字的分布情况

适合场景:适合查找比较小且连续的情况.

2.数字分析法

    如果关键字位数比较多,如手机号码,只有后四位的分布是用户号,其他位都有大量的重复,在后四位上数字分布均匀,就选后四位作为散列地址

适用场景:数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况.

3.平方取中法:

假设关键字是1234,平方就是1522756,再抽取中间的3位就是227作为散列地址.

假设关键字为4321,平方为18671041,抽取中间的三位就可以是671或者710用作散列地址.

使用场景:不知道关键字的分布,而位数又不是很大.

4.折叠法

1)将关键字从左到右分割成相等的几部分

2)再将这几部分求和,并按散列表表长,取后几位为散列地址.

如:9876543210,散列表表长3位,987+654+321+0=1962

,再求后三位得到散列地址为962.

适用场景:事先不需要知道关键字分布,适合关键字位数较多的情况.

5.除留取余法

f(key)=key%p(p<=m)

通常p为小于或者等于表长的最小质数或不含小于20质因数的合数

6.随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址

f(key)=random(key)

适用场景:关键字的长度不等

怎样处理冲突?

1.开放定址法

一旦找到了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入.

找空散列地址方法:

1)线性探查

fi(key)=(f(key)+di)%m  (di=1,2,3,...m-1)

2)二次探测

fi(key)=(f(key)+di)%m   (di=1^2,-1^2,2^2,-2^2,...)

3)随机探测法

fi(key)=(f(key)+di)%m   (d是一个随机数列)

开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,使我们最常用的解决冲突的办法.

2.再散列函数法

fi(key)=RHi(key)  (i=1,2,...,k)

    这里RHi就是不同的散列函数

优点:这种方法使得关键字不产生聚集

缺点:相应地增加了计算的时间

3.链地址法

    将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针.

优点:不会出现找不到地址的情况

缺点:查找时需要遍历单链表,有性能损耗

4.公共区溢出法:

所有冲突的关键字建立了一个公共的溢出区来存放.

    在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;

如果不相等,去溢出表顺序查找.

哈希表的实现与查找:

#include<iostream>
#include<cstdio>
using namespace std;
#define SUCCESS true
#define UNSUCCESS false
#define HASHSIZE 12
#define NULLKEY -32768
typedef bool Status;
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 SUCCESS;
}

//设定哈希函数
int Hash(int key)
{
	return key%m;
}

//输入关键字插入哈希表中
void InsertHash(HashTable *H, int key)
{
	int addr = Hash(key);
	//如果不为空,则冲突
	while (H->elem[addr] != NULL)
	{
		addr=(addr + 1) % m;//开放定址法的线性探测
	}
	//找到合适的地址,将元素放入
	H->elem[addr] = key;
}

//在哈希表中查找关键字
Status SearchHash(HashTable H, int key, int *addr)//addr为传出参数
{
	//求散列地址
	*addr = Hash(key);
	//如果不为key,则是发生冲突,继续查找
	while (H.elem[*addr] != key)
	{
		*addr = (*addr + 1) % m;//开放定址法的线性探测
		//如果下一个为空,或者又查询回到了原点,表示不存在
		if (H.elem[*addr] == NULLKEY || *addr == Hash(key))
		{
			return UNSUCCESS;
		}
	}
	return SUCCESS;
}

int main()
{
	int i,n,addr;
	int arr[HASHSIZE] = {12,67,56,16,25,37,22,29,15,47,48,43};
	HashTable h;
	bool ret=InitHashTable(&h);
	if (ret)
	{
		printf("初始化成功!\n");
	}
	else
	{
		printf("初始化失败!\n");
	}
	
	//往哈希里插入元素
	for (i = 0; i < HASHSIZE; ++i)
	{
		h.elem[i]=arr[i];
	}

	cin >> n;

	ret=SearchHash(h,n,&addr);
	if (ret)
	{
		printf("元素%d已找到,在第%d个位置\n",n,addr+1);
	}
	else
	{
		printf("该元素未在表中\n");
	}

	free(h.elem);

	return 0;

}
发布了40 篇原创文章 · 获赞 23 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/suoyudong/article/details/88656851