数据结构---哈希(Hash)


对于set和map来说底层所使用的是红黑树,其在搜索上面已经很厉害了,为什么还需要在搞一个unordered_set和unordered_map呢?

#include<iostream>
#include<vector>
#include<unordered_set>
#include<set>
#include<time.h>

using namespace std;

void test_time()
{
   
	int n = 1000000;
	vector<int> v;
	srand(time(0)); //初始化随机数发生器
	for (int i = 0; i < n; ++i)
	{
   
		v.push_back(rand()); //随机数发生器
	}

	set<int> s;
	size_t begin1 = clock();

	for (auto e : v)
	{
   
		s.insert(e);
	}
	size_t end1 = clock();

	cout << "set : " << end1 - begin1 << endl;


	unordered_set<int> us;
	size_t begin2 = clock();

	for (auto e : v)
	{
   
		us.insert(e);
	}
	size_t end2 = clock();

	cout << "unordered_set : " << end2 - begin2 << endl;
}

int main()
{
   
	test_time();
}

在这里插入图片描述
通过对于很多个数的插入(数越多,插入之间所消耗的时间差距越大),其实也是能够看出来HashTable的优越之处。

1. 哈希概念

可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。
在这里插入图片描述

2. 哈希函数

引起哈希冲突的一个原因可能是:哈希函数设计不够合理

哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0 到m-1之间。
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

常见哈希函数

  1. 直接定制法–(常用)
    取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况。

  2. 除留余数法–(常用)
    设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

  3. 平方取中法

  4. 折叠法

  5. 随机数法

  6. 数学分析法

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

3. 哈希冲突

解决哈希冲突两种常见的方法是:闭散列和开散列

3.1 闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  • 通过哈希函数获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
  • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
    在这里插入图片描述

载荷因子

  • 为了能够保证一直有空位置,所以这里引入了载荷因子,对于载荷因子来说,经过大量的使用最好能够控制在0.7以下最好,一旦超过就会大大增加哈希冲突的概率

在这里插入图片描述

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。(引发踩踏效应,就像我的位置被人占了,我就去占别人的位置,后来的人都效仿,会导致效率变低)

二次探测线性探测的缺陷是产生冲突的数据堆积在一块,这和找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,他不再是挨着找下一个空位置,而是平方式的跳跃找下一个空位置,这样冲突就不会堆积在一片,而是会相对散开一些。

比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

3.1.1 闭散列模拟实现

  1. 对于闭散列来说,每个哈希表对应的位置里面存储了数据和状态
  2. 其中数据的类型是由模板的第二个参数所控制的,模板参数T是Key或者pair<const K,V>,所以这也是一种更高维度的泛型。
  3. 其中对于删除来说,采用的是一种伪删除法。因为对于闭散列来说,最需要的就是找到下一个空位置,如果没有这个状态的话,原本有数据的位置删除了就会直接的变为空位置,那么如果删除的位置刚好在你的要查找的数据之前,那么就会误认为找不到,事实上,是存在的。
  4. 对于闭散列在增容的时候所采用的方法:重新开辟一个原先二倍大小的vector,然后把你原先的数据通过新的哈希函数计算位置,然后这个新的HashTable和你原先的进行交换。
namespace Close
{
   

	enum State
	{
   
		EMPTY,
		EXIST,
		DELETE
	};

	template<class T>
	struct HashNode
	{
   
		State _state = EMPTY;
		T _t;
	};

	template<class K>
	struct Hash
	{
   
		size_t operator()(const K& key)
		{
   
			return key;
		}
	};

	//模板特化
	template<>
	struct Hash < string >
	{
   
		size_t operator()(const string& s)
		{
   
			size_t hash = 0;
			for (auto ch : s)
			{
   
				//hash += ch;
				hash = hash * 131 + ch; //
			}
			return hash;
		}
	};
	//这里和使用红黑树来封装map和set是一样的,都是由第二个模板参数来控制存储的类型是K,还是pair<const K,V>
	template<class K, class T, class HashFunc = Hash<K>>//需要把这里的K转换为整形
	class HashTable
	{
   
	public:
		bool Insert(const T& t)
		{
   
			//载荷因子= 填入表中的元素个数 / 散列表的长度
			//负载因子>0.7(严格控制在0.7以下)
			//只有在这种情况下:效率相对较高,并且空间利用率也较好
			if (_tables.size() == 0 || _size * 10 / _tables.size() == 7)
			{
   
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				//但是这里是不对的,因为有可能原来冲突的,由于空间的扩容,而不再是冲突的了,
				//所以我们应该开辟好一段空间,然后重新对上面的数据进行计算,然后放入这段空间内,在释放原来的空间


				//vector<HashNode<T>> newtables;
				//newtables.resize(newsize);

				此时就是把原空间上的数据,拿来重新计算放到相对应的新空间上
				//for (size_t i = 0; i < _tables.size(); ++i)
				//{
   
				//	if (_tables[i]._state == EXIST)
				//	{
   
				//		//线性探测找在新表中的位置
				//	}
				//}
				//newtables.swap(_tables);

				HashTable<K, T, HashFunc> newht;
				newht._tables.resize(newsize);
				for (auto& e : _tables)
				{
   
					if (e._state == EXIST)
					{
   
						//重新计算位置,然后放在这个新的表中
						newht.Insert(e._t); //拿旧空间的数据重新计算我嫌弃太麻烦,我还不如直接重新来一遍插入呢
					}
				}
				_tables.swap
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值