【c++】哈希---unordered容器+闭散列+开散列

1.unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 logN,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

unordered_set:

1.  unordered_set是不按特定顺序存储键值的关联式容器,其允许通过键值快速的索引到对应的元素。
2.  在unordered_set中,元素的值同时也是唯一地标识它的key。
3.  在内部,unordered_set中的元素没有按照任何特定的顺序排序,为了能在常数范围内找

4.  到指定的key,unordered_set将相同哈希值的键值放在相同的桶中。
5.  unordered_set容器通过key访问单个元素要比set快,但它通常在遍历元素子集的范围迭代方面效率较低。
6.  它的迭代器至少是前向迭代器。

 

 部分接口的使用:

#include <iostream>
#include <unordered_set>
using namespace std;

int main()
{
	unordered_set<int> us;
	//插入元素(去重)
	us.insert(1);
	us.insert(4);
	us.insert(3);
	us.insert(3);
	us.insert(2);
	us.insert(2);
	us.insert(3);
	
	for (auto e : us)
	{
		cout << e << " ";
	}
	cout << endl; 
}

 unordered_map

1.  unordered_map是存储<key, value>键值对的关联式容器,其允许通过key值快速的索引到与其对应是value。
2.  在unordered_map中,键值通常用于唯一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
3.  在内部,unordered_map没有对<key, value>按照任何特定的顺序排序,为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
4.  unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
5.  unordered_map实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
6.  它的迭代器至少是前向迭代器。

代码练习:

int main()
{
	unordered_map<string, int> countMap;
	string arr[] = { "苹果","香蕉","苹果" ,"西瓜","西瓜"};
	for (auto& e : arr)
	{
		auto it = countMap.find(e);
		countMap[e]++;
	}
	for (auto& kv : countMap)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	return 0;
}

unordered_map容器当中还实现了[ ]运算符重载函数,该重载函数的功能非常强大:[key]

若当前容器中已有键值为key的键值对,则返回该键值对value的引用。
若当前容器中没有键值为key的键值对,则先插入键值对<key, value()>,然后再返回该键值对中value的引用。


2.哈希的概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O( ),搜索的效率取决于搜索过程中元素的比较次数。理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
哈希映射:key值跟存储位置建立关联关系。

哈希冲突:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。

哈希函数:
1. 直接定址法
取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
2. 除留余数法
设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,
按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址

3.解决哈希冲突

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

1.闭散列——开放定址法

插入数据

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。常用的寻找下一个位置的方法是线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

删除:

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索,线性探测采用标记的伪删除法来删除一个元素

扩容:

在有限的空间内,随着我们插入的数据越来越多,冲突的概率也越来越大,查找效率越来越低,所以闭散列的冲突表不可能让它满了,所以引入了负载因子:负载因子/载荷因子:等于表中的有效数据个数/表的大小,衡量表的满程度,在闭散列中负载因子不可能超过1(1代表满了)。一般情况下,负载因子一般在0.7左右。负载因子越小,冲突概率也越小,但是消耗的空间越大,负载因子越大,冲突概率越大,空间的利用率越高。当负载因子大于0.7的时候就需要进行扩容了:扩容不能进行直接拷贝,空间的大小发生变化映射的位置也会随着改变,所以需要重新计算映射的位置。

仿函数:考虑到统计出现次数:因为字符串不能够取模,所以我们可以给HashTable增加一个仿函数Hash,其可以将不能取模的类型转成可以取模的类型,同时把string特化出来解决字符串不能取模的问题

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

代码实现

#include <vector>
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 ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

namespace closehash
{
	enum State
	{
		EMPTY,  //空
		EXIST,  //存在
		DELETE  //删除
	};

	template<class K,class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;  
	};
	template<class K,class V,class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashData<K, V> Data;
	public:
		HashTable()
			:_n(0)
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))   //不允许有相同数据的元素插入
				return false;
			if (_n * 10 / _tables.size() >= 7)    //负载因子 到达比例就开始扩容
			{
				HashTable<K, V, Hash> newHT;  
				newHT._tables.resize(_tables.size() * 2);
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHT.Insert(e._kv);
					}
				}
				_tables.swap(newHT._tables);
			}
			Hash hf;
			size_t hashi = hf(kv.first)% _tables.size();  //求出原本该在的位置
			while (_tables[hashi]._state == EXIST)  //开始寻找没有被占用的位置
			{
				++hashi;
				hashi %= _tables.size();   //始终保持在vector内
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;
			return true;
		}

		Data* Find(const K& key)
		{
			Hash hf;
			size_t hashi = hf(key) % _tables.size();
			size_t starti = hashi;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == EXIST && key == _tables[hashi]._kv.first)
				{
					return &_tables[hashi];
				}
				++hashi;
				hashi %= _tables.size();
				//如果数据不存在就会导致一直循环的找,最多一圈
				if (hashi == starti)
				{
					break;
				}
			}
			return nullptr;
		}

		bool Erase(const K& key)  //伪删除
		{
			Data* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			else
			{
				return false;
			}
		}
	private:
		vector<Data> _tables;   
		size_t _n = 0;   //有效数据的个数
	};

	

2.开散列——开链法

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

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。由于桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容。开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突。所以在元素个数刚好等于桶的个数时,可以给哈希表增容 。研究分析表明:素数作为哈希表的长度可以尽可能减小哈希冲突。所以可提前定义一个素数表。

代码实现:

#include <vector>
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 ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};
namespace buckethash
{
	template<class K,class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;
		HashNode(const pair<K, V>& kv)
			:_kv(kv)
			, _next(nullptr)
		{}
	};
	template<class K,class V,class Hash=HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
			:_n(0)
		{
			_tables.resize(__stl_next_prime(0));
		}

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

				_tables[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			if (_tables.size() == _n)
			{
                
                //相当于是将节点从旧的哈希桶中搬到新的哈希桶中
				vector<Node*> newTables;
				newTables.resize(__stl_next_prime(_tables.size()), nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = Hash()(cur->_kv.first) % newTables.size();
						cur->_next = newTables[hashi];
						newTables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullptr;
				}
				_tables.swap(newTables);
			}

			size_t hashi = Hash()(kv.first) % _tables.size();
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
			return true;
		}

		Node* Find(const K& key)
		{
			size_t hashi = Hash()(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				else
				{
					cur = cur->_next;
				}
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			size_t hashi = Hash()(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (cur == _tables[hashi])
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					--_n;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}
		inline unsigned long __stl_next_prime(unsigned long n)
		{
			static const int __stl_num_primes = 28;
			static const unsigned long __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (int i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return __stl_prime_list[__stl_num_primes - 1];
		}

	private:
		vector<Node*> _tables;
		size_t _n = 0;
	};
	
}

3.开散列与闭散列比较

应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销 由于开地址法必须保持大量的空闲空间以确保搜索效率,如线性探查法要求负载因子 <= 0.7 ,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。


 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bite-ccc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值