查找(5)---散列表

散列表

方法:用算术操作将键转换为索引来访问数组中的键值对。

第一步是用散列函数将被查找的键转换为数组的一个索引。理想情况下每一个键都会有一个索引。当然这是理想情况,所以会有第二步,处理碰撞冲突的过程。

两种解决碰撞的方法:拉链法线性探测法

散列法是算法在时间和空间上做出权衡的经典例子。

在一般情况下,散列法都能达到常数级别的插入和查找操作,这也使得他在很多情况下成为实现符号表的最佳选择。


散列函数

严格来说,对于每种类型的键我们都需要一个与之对于的散列函数。如:


1)正整数

将整数散列最常用的方法是除留余数法。我们选择大小为素数M的数组,对于任意正整数K,计算K除以M的余数。

如果M不是素数,会导致我们无法均匀的散列散列值。


2)浮点数

如果是0~1之间的实数,我们可以将它乘以M并四舍五入得到一个0~M-1之间的索引值。这种方法,在建的高位其作用更大,但低位对散列的结果没有影响。修正的方法是将键表示为二进制在使用除留余数法。


3)字符串

除留余数法也可以处理较长的键,例如字符串,我们只需将他们当做大整数即可。我们将字符串当作R进制的值,将它除以M并取余。


3)组合键

如果键的类型含有多个整型变量,我们可以和string类型一样将他们混合起来。如时间:年月日

int hash=(((day*R+month)%M*R+year)%M;


总的来说,一个数据类型实现一个优秀的散列方法需要满足三个条件:

1)一致性——等价的键必然产生相等的散列值;

2)高效性——计算简便;

3)均匀性——均匀地散列所有的键。


散列表的构造方法:

1.拉链法

申请一个数组记录散列值的可能,然后每一个值衍生出一条链表,用以存储碰撞冲突,即相同的值存储在同一条链表中。

代码:

class SCHST
{
private:
	int M=0;//散列表长度
	vector<ST> st;//存放链表的数组//ST为顺序查找中的链表
	int hash(string key)//散列方法
	{
		int hash = 0, r = 8;//模拟八进制
		for (int i = 0; i < key.size(); i++)
			hash = (r*hash + key[i]) % M;
		return hash;
	}
public:
	SCHST(int m) :M(m),st(m){}
	int size()//键值对总数
	{
		int N = 0;
		for (int i = 0; i < M; ++i)
			N += st[i].size();
		return N;
	}
	string get(string key)//查找
	{
		return st[hash(key)].get(key);
	}
	void put(string key, string item)//插入
	{
		st[hash(key)].put(key, item);
	}
	void delete_1(string key)//删除
	{
		st[hash(key)].delete_1(key);
	}
	queue<string> Keys()//键的集合
	{
		queue<string> que,a;
		for (int i = 0; i < M; i++)
		{
			a = st[i].keys();
			while (!a.empty())
			{
				que.push(a.front());
				a.pop();
			}
		}
		return que;
	}
};


当我们的目标是选择适当的数组时,拉链法就会是一个好的选择,他不会因为空链表而浪费内存,也不会因为链表太长而浪费时间。当内存不紧张时,选择一个足够大的M,使得查找时间变为常数;当内存紧张时,选择尽量大的M仍然能够将性能提升M倍。

基于拉链的散列表最主要的目的在于均与将键散布,也就失去了键的顺序信息,所以在追求键的顺序是,他不是一个好的选择,但他可能是最快的(也是最广泛的)符号表的实现。


2.基于线性探测法

使用数组存储,当碰撞发生时,我们直接选择他的下一个位置(即索引加一)进行存储。

这样的线性探测会产生三种结果:

1)命中,该位置的键和被查找的键相同

2)未命中,键为空

3)继续查找,该位置的键和查找键不同

核心思想是将内存用在链表,不如将他们作为散列表的空元素,这些空元素可以作为查找结束的标志。

代码:

class LPHST
{
private:
	int N;//键值对总数
	int M=0;//线性探测表的大小
	vector<string> keys;//键
	vector<string> datas;//值
	int hash(string key)//散列方式
	{
		int hash = 0, r = 8;
		for (int i = 0; i < key.size(); i++)
			hash = (r*hash + key[i]) % M;
		return hash;
	}
public:
	LPHST(int m) :M(m), keys(m,"NULL"), datas(m,"NULL"){}
	int size(){ return N; }
	void put(string key, string item)//插入
	{
		int i;
		for (i = hash(key); keys[i] != "NULL";i=(i+1)%M)
			if (key == keys[i])
			{
				datas[i] == item;
				return;
			}
		keys[i] = key;
		datas[i] = item;
		N++;
	}
	string get(string key)//查找
	{
		for (int i = hash(key); keys[i] != "NULL"; i = (i + 1) % M)
			if (key == keys[i])return datas[i];
		return "No key";
	}
	bool contains(string key)//键是否存在
	{
		return get(key) != "No key";
	}
	void delete_1(string key)//删除
	{
		if (!contains(key))return;
		int i = hash(key);
		while (key != keys[i])//查询删除键
			i = (i + 1) % M;
		keys[i] = "NULL";//删除键
		datas[i] = "NULL";
		i = (i + 1) % M;
		while (keys[i] != "NULL")//向前移动删除键的后继
		{
			string k = keys[i];
			string item = datas[i];
			keys[i] = "NULL";
			datas[i] = "NULL";
			N--;
			put(k, item);
			i = (i + 1) % M;
		}
		N--;
	}
	queue<string> Keys()//键的集合
	{
		queue<string> que;
		for (int i = 0; i < M; i++)
			if(keys[i]!="NULL")
				que.push(keys[i]);
		return que;
	}
};

插入:

当一个新键的散列值为空元素时,那么就保持它,如果不是,我们就顺序查找下一个空元素保持它

查找:

要查找一个键,我们从它的散列值开始找,如果不是则顺序查找下一位,为空则为命中

删除:

查找到元素后,删除信息,然后将同一散列值的后序元素前移。


散列表可能的缺点:

1)每种类型的键都需要一个优秀的散列函数;

2)性能保证来自于散列函数的质量;

3)散列函数的技术可能是复杂而且昂贵的;

4)难以支持有序性相关的符号表操作。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值