C++——哈希(一)

1. unordered系列关联式容器

在了解哈希之前我们先简单了解一下unordered系列的关联式容器,因为其底层就是用哈希来实现的,其实也没啥好说的,C++11中,STL又提供了unordered系列的关联式容器(unordered_map和unordered_set),与红黑树结构的关联式容器(map和set)使用方式基本类似,就是其底层结构不一样而已

感兴趣的可以看一下链接

unordered_map:

https://cplusplus.com/reference/unordered_map/unordered_map/?kw=unordered_map

unordered_set:

https://cplusplus.com/reference/unordered_set/unordered_set/?kw=unordered_set

2.底层结构 

2.1哈希概念

哈希/散列其本质是存储的值和存储位置的映射

2.2哈希函数

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

常见的哈希函数:

直接定值法

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

 

除留余数法 

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

2.3哈希冲突

引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
 对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,
但有:Hash($k_i$) == Hash($k_j$)
即: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。

注意:哈希冲突是无法避免的(在有限的空间中存无限的值,不论哈希函数多精妙,哈希冲突的结果是必然的),不过哈希函数设计得越精妙那么产生哈希冲突的可能性就越低 

2.4解决哈希冲突 

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

2.4.1闭散列解决

闭散列的开放定址法(本质是当前位置冲突了,后面找一个位置继续存储就得了)
 常见两种
                1.线性探测                                  2.二次探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
以除留余数法的哈希冲突为例
 

8abd3012b0924664b0310775783aa843.png

 hash(key)可以看成下标,结合上图如果i位置已经有了,就线性往后查找到空位置放进去

查找

i=key%表的大小

如果i位置不是要找的key,就往后找直到找到或者遇到空,如果找到表示结尾的位置(还没遇到空),就要往头回绕了

插入

通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

删除

 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素

 啥是伪删除法呢?在表中的节点(存数据的)增加一个表状态的成员变量(空、存在、删除)

enum State//状态
{
	EMPTY,//空
	EXIST, //存在
	DELETE//删除
};
template<class K, class V>
struct HashData
{
	pair<K, V> _val;
	State _state = EMPTY; // 标记
};

 

线性探测优点:实现非常简单,
线性探测缺点: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降

 上文中说到将无限的数据插入到有限的空间中,随着空间中的元素越来越多,插入元素时产生哈希冲突的概率也就越来越大,既然冲突是必然发生的,那么多次冲突后查找的效率也就必然会降低

由此我们不得不考虑到这个问题:哈希表什么情况下进行扩容?如何扩容?

对于扩容,哈希表中增加了负载因子(载荷因子)

负载因子=表中数据的个数/空间大小

负载因子越大,表明表中填入的元素越多(空间利用率越高),产生冲突的可能性也就越大;反之,负载因子越小,表明表中填入的元素越少(空间利用率越低),产生冲突的可能性也就越小

对于开放定址法来说负载因子是一个特别重要的因素,最好控制在0.7~0.8以下,超过这个值应该对哈希表进行扩容

2.二次探测 

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位
置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题, 找下一个空位置的方法
为:
Hi = (H0+i^2)%m(i=1,2,3...)
Hi是冲突元素通过二次探测后得到的存放位置
H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。

 

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出
必须考虑增容。

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

2.4.2开散列(哈希桶)

1. 开散列概念
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。
cdb535e09377404e980a2a98cd461e64.png
 

数组里面存冲突的指针然后像链表一样插入

再讲一下极端条件下的情况

所有元素的哈希值均相同,最终都放到同一个哈希桶中,此时这个哈希表的增删查改效率会降低

有的实现方法是当桶中元素个数超过一定长度,会把桶中的链表结构改成红黑树结构

d88b612a73f94f608cc29c2d9b3f6205.png

当然不做这个处理也行,反正数据一多负载因子会增长,最后触发扩容条件

3.实现

3.1线性探测的实现

前面线性探测解决哈希冲突中我们提到不能直接删除哈希表中的元素,要采用伪删除

	enum State//枚举状态
	{
		EMPTY,//空
		EXIST, //存在
		DELETE//删除
	};
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _val;
		State _state = EMPTY; // 标记
	};

哈希表的构造和框架

初始化的时候顺便把扩容的事情搞定

template<class K, class V>
class HashTable
{

public:
	HashTable(size_t size = 10)
	{
		_ht.resize(size);
	}
private:
	vector<HashData<K, V>> _ht;//hashtable
	size_t _n;
};

查找和插入的思路在闭散列解决哈希冲突中已经说过我就不重复讲了

查找

// 查找
HashData<K, V>* Find(const K& key)
{
	size_t hashi = key % _ht.size();
	while (_ht[hashi]._state != EMPTY)//不等于空说明有存东西
	{
		if (_ht[hashi]._val.first == key)
		{
			return &_ht[hashi];
		}
		hashi++;
		hashi %= _ht.size();
	}
	return nullptr;
}

插入

不过扩容的地方要注意,要讲旧表的数据一个个插入新表中,要重新计算哈希值(也就是下标)

bool Insert(const pair<K, V>& val)
{
	
	if (Find(val.first))
		return false;
	//扩容
	if (_n * 10 / _ht.size() >= 7)
	{
		HashTable<K, V> newtable(_ht.size() * 2);
		for (auto& e : _ht)
		{
			if (e._state == EXIST)
			{
				newtable.Insert(e._val);
			}
		}
		_ht.swap(newtable._ht);
	}
			//线性探测
	size_t hashi = val.first % _ht.size();
	while (_ht[hashi]._state == EXIST)//如果位置被占了往后走
	{
		hashi++;
		hashi %= _ht.size();//中间走到后面,然后超过后面返回前面
	}
	//		找到空位放进去
	_ht[hashi]._val = val;
	_ht[hashi]._state = EXIST;
	_n++;
	return true;
}

删除

删除的步骤也是非常简单啊,find找一下有没有这个值,然后状态改为删除,哈希表中的有效个数-1就得了

bool Erase(const K& key)
{
	HashData<K, V>* ret = Find(key);
	if (ret)
	{
		_n--;
		ret->_state = DELETE;
		return true;
	}
	return false;
}

源代码

#pragma once
#include<vector>

namespace Close_Hash
{
	enum State//状态
	{
		EMPTY,//空
		EXIST, //存在
		DELETE//删除
	};
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _val;
		State _state = EMPTY; // 标记
	};
	template<class K, class V>
	class HashTable
	{


	public:
		HashTable(size_t size = 10)
		{
			_ht.resize(size);
		}

		// 查找
		HashData<K, V>* Find(const K& key)
		{
			size_t hashi = key % _ht.size();
			while (_ht[hashi]._state != EMPTY)//不等于空说明有存东西
			{
				if (_ht[hashi]._val.first == key)
				{
					return &_ht[hashi];
				}
				hashi++;
				hashi %= _ht.size();
			}
			return nullptr;
		}

		// 插入
		bool Insert(const pair<K, V>& val)
		{
	
			if (Find(val.first))
				return false;
			//扩容
			if (_n * 10 / _ht.size() >= 7)
			{
				HashTable<K, V> newtable(_ht.size() * 2);
				for (auto& e : _ht)
				{
					if (e._state == EXIST)
					{
						newtable.Insert(e._val);
					}
				}
				_ht.swap(newtable._ht);
			}
			//线性探测
			size_t hashi = val.first % _ht.size();
			while (_ht[hashi]._state == EXIST)//如果位置被占了往后走
			{
				hashi++;
				hashi %= _ht.size();//中间走到后面,然后超过后面返回前面
			}
			//		找到空位放进去
			_ht[hashi]._val = val;
			_ht[hashi]._state = EXIST;
			_n++;
			return true;
		}

		// 删除
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				_n--;
				ret->_state = DELETE;
				return true;
			}
			return false;
		}



	private:
		size_t HashFunc(const K& key)
		{
			return key % _ht.capacity();
		}

	private:
		vector<HashData<K, V>> _ht;//hashtable
		size_t _n;
	};


	void TestHT1()
	{
		int a[] = { 1,4,24,34,7,44,17,37 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}

		for (auto e : a)
		{
			auto ret = ht.Find(e);
			if (ret)
			{
				cout << ret->_val.first << endl;
			}
			else
			{
				cout << ret->_val.first << ":N" << endl;
			}
		}
		cout << endl;

		ht.Erase(34);
		ht.Erase(4);

		for (auto e : a)
		{
			auto ret = ht.Find(e);
			if (ret)
			{
				cout << ret->_val.first << endl;
			}
			else
			{
				cout << e << ":N" << endl;
			}
		}
		cout << endl;
	}

}

3.2哈希桶的实现

表由多个桶组成(表是数组),桶(链表)里面的节点

template <class K,class V>
struct HashNode//节点
{
	HashNode<K,V>* _next;
	pair<K, V> _kv;
	HashNode(const pair<K,V>& kv)
		:_next(nullptr)
		,_kv(kv)
	{}
};

哈希表的框架

template<class K, class V>
class HashTable
{
	
	typedef HashNode<K,V> Node;
public:
    //。。。
private:
	vector<Node*> _tables; // 指针数组
	size_t _n;
};

构造

HashTable()
{
	_tables.resize(10, nullptr);
	_n=0;
}

查找

对传来的值找下表,找不到返回空,找到了遍历这个桶(链表),如果找到了返回那个节点,找不到继续往下走直到走到空

		Node* Find(const K& key)
		{
			size_t hashi = key % _tables.size();//找下标
			Node* cur = _tables[hashi];
			while (cur)//走链表
			{
				if (cur->_kv.first == key)//找到了
				{
					return cur;
				}
				cur = cur->_next;//往下走
			}
			//找不到
			return nullptr;
		}

 

插入

思路和线性探测的插入思路差不多

找那个值,如果已经有了那就不用插入了

如果没有那么就找到这个值对应的下标,然后头插入桶(链表),然后有效元素个数+1

判断负载因子看看要不要扩容,扩容还是创建新表,遍历旧表插入新表(节点挂到新表的下标可能不一样),再交换一下新旧表

		bool Insert(const pair<K, V>& kv)
		{
			//插入失败(找不到)
			if (Find(kv.first))
				return false;
			//扩容
			if (_n == _tables.size())//负载因子极限
			{
				vector< Node*> newtable(_tables.size() * 2, nullptr);
				//把旧桶的节点挂到新表新桶中
				for (size_t i=0;i<_tables.size();i++)
				{
					Node* cur = _tables[i];//遍历走一个个旧桶
					while (cur)
					{
						Node* next = cur->_next;//存一下next,待会可以往下走
						size_t hashi = cur->_kv.first % newtable.size();
                        //节点挂到新桶的下标可能不一样
						//挂上新桶
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						//往下走
						cur = next;
					}
					//把旧桶被移走的节点置空
					_tables[i] = nullptr;
				}
				_tables.swap(newtable);
			}
			size_t hashi = kv.first % _tables.size();//找到下标
			Node* newnode = new Node(kv);//开一个节点的空间
			//头插(入桶)
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;

		}

删除

删除的思路不一样(哈希桶可以直接删节点)

找下标,创建一个前驱节点(链表的删除嘛)

遍历对应下标的桶,如果找到的不是头节点,前驱直接指向遍历节点的下一个;如果找到的是头节点,头节点直接成遍历节点的下一个。最后delete删除遍历节点,减一下有效元素个数

bool Erase(const K& key)
{
	size_t hashi = key % _tables.size();
	Node* prev = nullptr;//保存遍历节点的上一个
	Node* cur = _tables[hashi];
	while (cur)	
	{

		if (cur->_kv.first == key)//找到
		{
			//删除
			if (prev)//节点有可能是头节点  prev非空,不是头节点
			{
				prev->_next = cur->_next;
			}
			else//要删除的节点是头节点
			{
				_tables[hashi] = cur->_next;
			}
			
			delete cur;
			_n--;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

源码

#pragma once
#include <vector>
版本1
namespace HashBucket
{
	template <class K,class V>
	struct HashNode//表的节点(桶口)
	{
		HashNode<K,V>* _next;
		pair<K, V> _kv;
		HashNode(const pair<K,V>& kv)
			:_next(nullptr)
			,_kv(kv)
		{}
	};
	template<class K, class V>
	class HashTable
	{
	
		typedef HashNode<K,V> Node;
	public:
		HashTable()
		{
			_tables.resize(10, nullptr);
			_n=0;
		}
		Node* Find(const K& key)
		{
			size_t hashi = key % _tables.size();//找下标
			Node* cur = _tables[hashi];
			while (cur)//走链表
			{
				if (cur->_kv.first == key)//找到了
				{
					return cur;
				}
				cur = cur->_next;//往下走
			}
			//找不到
			return nullptr;
		}

		bool Insert(const pair<K, V>& kv)
		{
			//插入失败(找不到)
			if (Find(kv.first))
				return false;
			//扩容
			if (_n == _tables.size())//负载因子极限
			{
				vector< Node*> newtable(_tables.size() * 2, nullptr);
				//把旧桶的节点挂到新表新桶中
				for (size_t i=0;i<_tables.size();i++)
				{
					Node* cur = _tables[i];//遍历走一个个旧桶
					while (cur)
					{
						Node* next = cur->_next;//存一下next,待会可以往下走
						size_t hashi = cur->_kv.first % newtable.size();//节点挂到新桶的下标可能不一样
						//挂上新桶
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						//往下走
						cur = next;
					}
					//把旧桶被移走的节点置空
					_tables[i] = nullptr;
				}
				_tables.swap(newtable);
			}
			size_t hashi = kv.first % _tables.size();//找到下标
			Node* newnode = new Node(kv);//开一个节点的空间
			//头插(入桶)
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;

		}
		bool Erase(const K& key)
		{
			size_t hashi = key % _tables.size();
			Node* prev = nullptr;//保存遍历节点的上一个
			Node* cur = _tables[hashi];
			while (cur)	
			{

				if (cur->_kv.first == key)//找到
				{
					//删除
					if (prev)//节点有可能是头节点  prev非空,不是头节点
					{
						prev->_next = cur->_next;
					}
					else//要删除的节点是头节点
					{
						_tables[hashi] = cur->_next;
					}
					
					delete cur;
					_n--;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}
	 
	private:
		vector<Node*> _tables; // 指针数组
		size_t _n;
	};

	void TestHB1()
	{
		int a[] = { 15,7,8,32,24,5,6,9,13,14,19,22,24,17};
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert(make_pair(e, e));
		}
		for (auto e : a)
		{
			auto ret = ht.Find(e);
			if (ret)
			{
				cout << ret->_kv.first <<":EXIST" << endl;
			}
			else
			{
				cout << ret->_kv.first << ":DELETE"<< endl;
			}
		}
		cout << endl;
		ht.Erase(24);
		ht.Erase(32);
		for (auto e : a)
		{
			auto ret = ht.Find(e);
			if (ret)
			{
				cout << ret->_kv.first << ":EXIST" << endl;
			}
			else
			{
				cout << e << ":DELETE" << endl;
			}
		}

	}
}

 

 

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值