C++ 哈希(unorderedmap/unorderedset)

1. unordered_map、unordered_set

1.1 是什么

        这两者底层采用了哈希表来实现,在功能上没有map和set的排序功能,因为哈希表本身是不具备二叉搜索树的性质的,但是哈希算法使得unordered_map、unordered_set的搜索效率是比红黑树高的。

1.2 哈希的概念

        拿一个书店来举例子,顾客可以在书店中借书,店主需要通过顾客借书的天数来进行相应的收费。那么店主可以在本子上记录下顾客的名字(该本子上按照字母序来记录顾客名),下次顾客再来还书的时候,店主就可以像翻字典一样,在本子上翻到顾客借书的信息。

        这种将顾客名字的首字母和顾客信息产生关联的方式就叫做哈希。

        也就是说对于任意的信息,我们只要找到该信息和某个值能产生联系,并且在计算机用相应的数据结构表示出这种关系即可。一般在C++中我们会使用vecotr数组来存放哈希表,通过将任意的信息通过算法设计(哈希函数)和vector的下标产生关联,这样在计算机中寻找的时候就很容易了。

1.3 哈希冲突

        对于两个信息kv1和kv2,对于这两者如果我们通过哈希函数计算出存储的位置是相同的,这种情况就被称为哈希冲突,一般而言在哈希表中,值和存储位置是多对一的关系。

2 .哈希函数

2.1  直接定值法

        比如在统计一个字符串中每个小写字母出现的次数,在这里我们可以使用红黑树的map来解决问题,我们也可以使用一个哈希表来解决,将小写字母 a 到 z 的ASCII码值和数组的下标做映射就可以了,ASCII码为97 的 a 存在数组的第一个位置,以此类推。

        这种方法只适合数据量比较小而且数据比较集中的情况,如果数据量非常分散的话,我们就需要把数组开的很大,虽然可以实现目的但是浪费了很多存储空间。

2.2 除留余数法

        Hash(key)= key % n (n为vector存储元素个数的大小)

        以这种方式存储数据可以比直接定值法节约很多空间,但是会出现哈希碰撞的情况。

3. 哈希碰撞的解决

3.1 闭散列(开放定值法)

        在发生哈希冲突时,如果哈希表在还没有满的情况下,我就把当前的数据放到冲突位置的下一个空的位置上去(问哦的位置被占了,我就占用别人的位置)

寻找空的位置我们有两种方式去寻找

3.1.1 线性探测

        该哈希表采用除留余数法作为哈希函数,从发生冲突的位置开始,依次向vector的下一个位置探测,直到探测到一个空的位置。

(1)插入

        在插入时先用除留余数法算出当前信息与vector中下标的映射关系,如果发生哈希冲突后,就一直向下一个位置探测,直到找到一个空的位置,最后插入该信息。

(2)删除

        使用闭散列的结构再删除元素时,我们不能真正物理上删除掉这个元素,因为我们在查找时,会根据哈希函数算出相应的vector下标,在vector中从算出的下标位置一直向后寻找,直到碰到有节点是空为止。所以如果真的把一个值物理删除的话会影响到查找函数的运行。

        所以我们对于每一个vector中的节点我们都需要定义三个状态即:EXIST/EMPTY/DELETE

        这三种状态分别代表 某元素在vector中是存在的/ 不存在的 / 被删除的。

(3)扩容

  这里在哈希表中引入了负载因子的概念,所谓负载因子a = 表中现存元素的个数 / 散列表的长度

负载因子,代表着表中元素的多少,a越大说明表中元素越多,产生冲突的可能个就越大,空间利用率就越大,反之亦然,a越小,表中元素越小,产生冲突的可能就越小。

  对于开放定址法,负载因子一般控制在0.7-0.8左右。

3.1.2 二次探测法

与线性探测法类似,只不过是往n位置的 2 的 i 次方向后探测(i>=0),还是要占用其他元素的空间只不过比线性探测法好一些,还是不能根本的解决问题。

3.2 开散列

        开散列(拉链法、哈希桶),第一步还是使用除留余数法的哈希函数求出一个散列地址(在vector中存在哪)发生哈希碰撞后,用一个链表把发生碰撞的元素串起来,vector中个每个位置只要存放链表的头节点的地址就可以了。

        对于开散列而言也是需要扩容的,如果不扩容的话,结构上确实没啥问题,但是每一个节点的链表就会变得很长,在JAVA中,其解决方式为如果一个节点上存放的链表长度大于8,那么就把该链表转化为一个红黑树,这样对于较长链表的情况也不用担心效率的问题了。

        在C++中我们直接采用扩容的方法就可以了,如果负载因子大于等于1时,我们就需要扩容,这样做的目的是保证每一个节点的链表中的元素只有一个/每个桶中只有一个元素。

4.整体代码

4.1 闭散列

#pragma once
#include<vector>
#include<string>
#include<list>
using namespace std;
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 31;
			hash += e;
		}
		return hash;
	}

};
namespace open_address//开放定址法(闭散列)在一个空间里去找位置
{
	enum Status
	{
		EMPTY,
		EXIST,
		DELETE
	};//枚举状态,用来标记表中的某个元素是存在还是不存在

	template<class K,class V>
	struct HashData
	{
		pair<K, V> _kv;
		Status _s;//标记元素的状态
	};

	
	//struct HashFuncString
	//{
	//	size_t operator()(const string& key)
	//	{
	//		size_t hash = 0;
	//		for (auto e : key)
	//		{
	//			hash *= 31;//避免字母全相同但是顺序不同的情况
	//			hash += e;
	//		}
	//		return hash;
	//	}
	//};
	template<class K,class V,class Hash=HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;
			// 负载因子 = 存储数据的个数 / 整个容器的空间
			if (_n*10 / _table.size() == 7)//负载因子到0.7就扩容
			{
				size_t newSize = _tables.size()*2;
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(newSize);
				//遍历旧表
				for (size_ti = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._s == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newHT._tables);//扩容复用insert,不需要自己再写一次映射逻辑
			}
			Hash hf;
			//线性探测
			size_t hashi = hf(kv.(first)) % _tables.size();
			while (_tables[hashi]._s == EXIST)
			{
				hashi++;//如果想要插入的位置已经有值了那么就往后面一个位置插入
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._s = EXIST;// 找到空的位置插入,并且该位置的状态修改为存在
			return true;
		}
		HashData<K, V>* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(kv.first) % _tables.size();
			while (_tables[hashi]._s != EMPTY)
			{ 
				if (_tables[hashi]._s==EXIST &&
					_tables[hashi]._kv.first == key)
				{
					return &_tables.[hashi];
				}
				hashi++;//不为空继续往后走
				hashi %= _tables.size();
			}
			return NULL;
		}
		bool Erase(const K& key)//伪删除法
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_s = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}
		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._s == EXIST)
				{
					printf("[%d]->%d\n", i, _tables[i]._kv.first);
				}
				else if (_tables[i]._s == EMPTY)
				{
					printf("%d->E\n", i);
				}
				else
				{
					printf("[%d]->D\n", i);
				}
			}
		}
	private:
		vector<HashData> _tables;
		size_t _n = 0; //存储关键字的个数
	};
}

4.2 开散列

namespace hash_bucket {
	template<class K, class V>
	struct HashDate {
		pair<K, V> _kv;
		HashDate<K, V>* _next;

		HashDate(const pair<K, V>& kv) :_kv(kv), _next(nullptr) {

		}
	};

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable {
		typedef HashDate<K, V> Node;
	public:
		HashTable() {
			_table.resize(10);
		}
		
		HashTable(const HashTable<K, V, HashFunc>& ht) {
			//resize到表的容量(vector的size)
			_table.resize(ht.capacity());
			for (size_t i = 0; i < ht._table.size(); ++i) {
				Node* cur = ht._table[i];
				while (cur) {
					insert(cur->_kv);
					cur = cur->_next;
				}
			}
		}

		~HashTable() {
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				while (cur) {
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
			}
		}

		bool insert(const pair<K, V>& kv) {
			if (find(kv.first)) {
				return false;
			}
			size_t size = _table.size(); 
			HashFunc hf;
			if (_n == size) {
				size_t newSize = size * 2;
				//建立新标
				vector <Node*> newTable;
				newTable.resize(newSize);
				for (size_t i = 0; i < size; ++i) {
					//依次取下来旧表中的结点重新建立映射关系后头插到新表
					Node* cur = _table[i];
					while (cur) {
						Node* next = cur->_next;
						size_t hashi = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				//最后交换一下
				_table.swap(newTable);
			}
			
			size_t hashi = hf(kv.first) % size;
			Node* newNode = new Node(kv);

			//头插
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			++_n;
			return true;
		}
		bool erase(const K& key) {
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur) {
				if (cur->_kv.first == key) {
					//头删
					if (prev == nullptr) {
						_table[hashi] = cur->_next;
					}
					//中间位置删除
					else {
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}
		Node* find(const K& key) {
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur) {
				if (cur->_kv.first == key) {
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		size_t size() const {
			return _n;
		}

		size_t capacity() const {
			return _table.size();
		}

		void print() {
			for (size_t i = 0; i < _table.size(); ++i) {
				Node* cur = _table[i];
				printf("[%d]->", i);
				while (cur) {
					cout << cur->_kv.first << ':' << cur->_kv.second << "->";
					cur = cur->_next;
				}
				puts("NULL");
			}
			cout << endl;
		}
	private:
		//指针数组
		vector <Node*> _table;
		size_t _n = 0;
	};
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值