【C++】哈希

一、unordered系列容器

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

unordered_xxx系列与map和set容器的用法上几乎没有任何区别;
区别:

  1. unordered_xxx系列都是哈希表作为底层的,而map和set是用红黑树作为底层的
  2. unordered_xxx系列不排序,只去重
  3. unordered_xxx系列是单项迭代器,map和set是双向迭代器

二、unordered_set

在这里插入图片描述

unordered_set是一种容器,它以无特定顺序的方式存储唯一的元素,并允许根据元素的值快速检索各个元素。
在unordered_set中,元素的值同时也是它的键,唯一标识该元素。键是不可变的,因此,unordered_set中的元素一旦放入容器后就不能被修改,不过可以插入和删除。
在内部,unordered_set中的元素不按任何特定顺序排序,而是根据它们的哈希值组织成桶,以便通过它们的值(平均具有恒定的平均时间复杂度)直接快速访问各个元素。
对于通过键访问单个元素,unordered_set容器比set容器更快,尽管对于通过子集范围迭代它们通常效率较低。
容器中的迭代器是单向迭代器。

void hash_test()
{
	unordered_set<int> s;
	//set<int> s;

	s.insert(2);
	s.insert(4);
	s.insert(9);
	s.insert(1);
	s.insert(2);
	s.insert(3);

	for (auto e : s)
	{
		cout << e << " ";
	}
}

在这里插入图片描述

三、unordered_map

在这里插入图片描述

无序映射是关联容器,存储由键值和映射值组合形成的元素,并允许根据它们的键快速检索各个元素。
在unordered_map中,键值通常用于唯一标识元素,而映射值是与该键关联的对象的内容。键和映射值的类型可以不同。
在内部,unordered_map中的元素不根据它们的键或映射值按任何特定顺序排序,而是根据它们的哈希值组织成桶,以便通过它们的键值直接快速访问各个元素(平均具有恒定的平均时间复杂度)。
对于通过键访问单个元素,unordered_map容器比map容器更快,尽管对于通过子集范围迭代它们通常效率较低。
无序映射实现了直接访问运算符(operator[]),允许使用键值作为参数直接访问映射值。
容器中的迭代器是单向迭代器。

void hash_test2()
{
	unordered_map<string, string> dict;
	dict["insert"] = "插入";
	dict["sort"] = "排序";
	dict["delete"] = "删除";
	dict["string"] = "字符串";
	dict["insert"] = "xxxxx";
	dict.insert(make_pair("iterator", "迭代器"));
	unordered_map<string, string>::iterator umit = dict.begin();
	while (umit != dict.end())
	{
		cout << umit->first << ":" << umit->second << endl;
		umit++;
	}
	cout << endl;
}

在这里插入图片描述

四、set与unordered_set比较

void test_op()
{
	int n = 10000000;
	vector<int> v;
	v.reserve(n);
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		//v.push_back(rand());//无序数列
		//v.push_back(rand() + i);//无序数列+减少重复值
		v.push_back(i);//有序数列
	}

	size_t begin1 = clock();
	set<int> s;
	for (auto e : v)
		s.insert(e);
	size_t end1 = clock();

	size_t begin2 = clock();
	unordered_set<int> us;
	for (auto e : v)
		us.insert(e);
	size_t end2 = clock();
	cout << s.size() << endl; cout << us.size() << endl;
	cout << "set.insert: " << end1 - begin1 << endl;
	cout << "ordered_set.insert: " << end2 - begin2 << endl;

	size_t begin3 = clock();
	for (auto e : v)
		s.find(e);
	size_t end3 = clock();
	size_t begin4 = clock();
	for (auto e : v)
		us.find(e);
	size_t end4 = clock();
	cout << "set.find: " << end3 - begin3 << endl;
	cout << "ordered_set.find: " << end4 - begin4 << endl;

	size_t begin5 = clock();
	for (auto e : v)
		s.erase(e);
	size_t end5 = clock();
	size_t begin6 = clock();
	for (auto e : v)
		us.erase(e);
	size_t end6 = clock();
	cout << "set.erase: " << end5 - begin5 << endl;
	cout << "ordered_set.erase: " << end6 - begin6 << endl;
}

在这里插入图片描述
在这里插入图片描述
可见,unordered_set优势显而易见。

五、查找比较

  1. 暴力
    时间复杂度是O(N)

  2. 二分
    时间复杂度(logN)

  3. 平衡树
    时间复杂度(logN)

  4. 哈希

1.闭散列—开放定值法

a.线性探测
b.二次探测

2.开散列–拉链法/哈希桶

六、哈希

闭散列

温馨提醒:这里有个线性探测和二次探测,两者代码为继承关系,冗余度较高,请注意差别

namespace fzc
{
	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 HashTable
	{
	public:
		bool Insert(const pair<K, V>& kv)
		{
			//负载因子到了就扩容
			if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)//扩容
			{
				size_t newSize = _size == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newTable;
				newTable._tables.resize(newSize);
				for (auto e : _tables)
				{
					if (e._state == EXIST)
					{
						newTable.Insert(e._kv);
					}
				}
				_tables.swap(newTable._tables);
			}
			size_t hashi = kv.first % _tables.size();
			//线性探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_size;
			return true;
		}
	private:
		//vector<pair<K, V>> _tables;
		vector<HashData<K, V>> _tables;
		size_t _size = 0;//存储的有效数据个数
	};

	void testHT1()
	{
		HashTable<int, int> HT;
		HT.Insert(make_pair(1, 2));
		HT.Insert(make_pair(7, 2));
		HT.Insert(make_pair(6, 2));
		HT.Insert(make_pair(5, 2));
		HT.Insert(make_pair(2, 2));
		HT.Insert(make_pair(11, 2));
		HT.Insert(make_pair(3, 2));
		HT.Insert(make_pair(43, 2));
		HT.Insert(make_pair(23, 2));
		HT.Insert(make_pair(13, 2));
	}
}

看下来的代码

namespace fzc
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template <class K>
	struct HashFunc
	{

	};

	template<class K, class V>
	class HashTable
	{
	public:
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			//负载因子到了就扩容
			if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)//扩容
			{
				size_t newSize = _size == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V> newTable;
				newTable._tables.resize(newSize);
				for (auto e : _tables)
				{
					if (e._state == EXIST)
					{
						newTable.Insert(e._kv);
					}
				}
				_tables.swap(newTable._tables);
			}
			size_t hashi = kv.first % _tables.size();
			//线性探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_size;
			return true;
		}
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_size;
				return true;
			}
			return false;
		}
		HashData<K, V>* Find(const K& key)
		{
			if (_size == 0)
				return nullptr;

			size_t hashi = key % _tables.size();
			size_t start = hashi;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}
				hashi++;
				hashi %= _tables.size();

				if (hashi == start)//预防重复查找
					break;
			}
			return nullptr;
		}
		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					printf("[%d:%d] ", i, _tables[i]._kv.first);
				}
				else
				{
					printf("[%d:*] ", i);
				}
			}
			cout << endl;
		}
	private:
		//vector<pair<K, V>> _tables;
		vector<HashData<K, V>> _tables;//_tables.size()表示哈希表的大小
		size_t _size = 0;//存储的有效数据个数
	};

	void testHT1()
	{
		HashTable<int, int> HT;
		int a[] = { 1,7,6,5,2,11,3,43,23,13 };
		for (auto& e : a)
		{
			HT.Insert(make_pair(e, e));
		}

		HT.Erase(13);
		cout << HT.Find(23) << endl;
		cout << HT.Find(13) << endl;
		HT.Print();

		HT.Insert(make_pair(-2, -2));
		HT.Print();
	}
	void testHT2()
	{
		string arr[] = { "梨子","苹果","猕猴桃","桃" ,"梨子","苹果", "猕猴桃","猕猴桃","猕猴桃","梨子","猕猴桃" };
		HashTable<string, int> HT;
		for (auto& e : arr)
		{
			auto ptr = HT.Find(e);
			if (ptr)
			{
				ptr->_kv.second++;
			}
			else
			{
				HT.Insert(make_pair(e, 1));
			}
		}
	}
}

string类型无法进行取模运算,去看STL库:
在这里插入图片描述

hash< Key >是一个仿函数;来处理;
将key转化为无符号的类型,让其进行布尔值比较;

在这里插入图片描述

可能出现 abcd bcda 等顺序不同,但字母(或ASCII码)及其数量相同的字符串

看大佬的处理方法:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
完美解决;这些思想都与数学算法思想有关,感兴趣伙伴可了解一下;
完整代码:

namespace fzc
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	//仿函数
	template <class K>
	struct HashFunc
	{
		size_t operator()(const K& key)
		{
			return (size_t)key;
		}
	};

	//string类型属于常见类型,故而可做特化处理
	template <>
	struct HashFunc<string>//特化
	{
		//可能出现 abcd   bcda 等顺序不同,但字母(或ASCII码)及其数量相同的字符串,
		//BKDR 思想
		size_t operator()(const string& key)
		{
			size_t res = 0;
			for (auto e : key)
			{
				 res *= 131;
				res += e;
			}
			return res;
		}
	};
	//struct HashFuncString
	//{
	//	size_t operator()(const string& key)
	//	{
	//		size_t res = 0;
	//		for (auto e : key)
	//		{
	//			res += e;
	//		}
	//		return res;
	//	}
	//};

	template<class K, class V,class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			//负载因子到了就扩容
			if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7)//扩容
			{
				size_t newSize = _size == 0 ? 10 : _tables.size() * 2;
				HashTable<K, V,Hash> newHashTable;
				newHashTable._tables.resize(newSize);
				for (const auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						newHashTable.Insert(e._kv);
					}
				}
				_tables.swap(newHashTable._tables);
			}
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();
			//线性探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_size;
			return true;
		}
		bool Erase(const K& key)
		{
			HashData<K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_size;
				return true;
			}
			return false;
		}
		HashData<K, V>* Find(const K& key)
		{
			if (_size == 0)
				return nullptr;
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			size_t start = hashi;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}
				hashi++;
				hashi %= _tables.size();

				if (hashi == start)//预防重复查找
					break;
			}
			return nullptr;
		}
		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					//printf("[%d:%d] ", i, _tables[i]._kv.first);
					cout << i << "[" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << "]";
				}
				else
				{
					//printf("[%d:*] ", i);
					cout << i << "[" << _tables[i]._kv.first << ":" << _tables[i]._kv.second << "]";
				}
			}
			cout << endl;
		}
	private:
		//vector<pair<K, V>> _tables;
		vector<HashData<K, V>> _tables;//_tables.size()表示哈希表的大小
		size_t _size = 0;//存储的有效数据个数
	};

	void testHT1()
	{
		HashTable<int, int> HT;
		int a[] = { 1,7,6,5,2,11,3,43,23,13 };
		for (auto& e : a)
		{
			HT.Insert(make_pair(e, e));
		}

		HT.Erase(13);
		cout << HT.Find(23) << endl;
		cout << HT.Find(13) << endl;
		HT.Print();

		HT.Insert(make_pair(-2, -2));
		HT.Print();
	}
	void testHT2()
	{
		string arr[] = { "梨子","苹果","猕猴桃","桃" ,"梨子","苹果", "猕猴桃","猕猴桃","猕猴桃","梨子","猕猴桃" };
		//HashTable<string, int, HashFuncString> HT;
		HashTable<string, int> HT;
		for (auto& e : arr)
		{
			auto ptr = HT.Find(e);
			if (ptr)
			{
				ptr->_kv.second++;
			}
			else
			{
				HT.Insert(make_pair(e, 1));
			}
		}
		HT.Print();
	}
	void testHT3()
	{
		HashFunc<string> hash;
		cout << hash("abcd") << endl;
		cout << hash("bcda") << endl;
		cout << hash("acbd") << endl;
	}

}

以上为线性测试


“二次探测”
对比:
在这里插入图片描述
还是没有从本质上解决问题,仍为占用式(自身位置被占用,采用别人的),只是在应对集中式的数据时处理有所优化 ;
线性探测:让其挨着占用;
二次探测 :本质是让其跳跃占用
以上两种探测法均属于闭散列;下来看用开散列

采用开散列

拉链法
在这里插入图片描述
stlstl中负载因子为1

这里将其中可能存在的问题均在代码注释之中,若有小伙伴有问题评论区见

namespace hash_bucket
{
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		struct HashData* next = nullptr;
		HashData(const pair<K, V>& kv)
			:_kv(kv)
		{}
	};

	//仿函数:这里直接用开散列仿函数
	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 res = 0;
			for (auto e : key)
			{
				res *= 131;
				res += e;
			}
			return res;
		}
	};

	template<class K, class V, class Hash = HashFunc<K>>	//
	class HashTable
	{
		typedef HashData<K, V> Node;
	public:
		HashTable()
			:_size(0)
			,_tables(10, nullptr)
		{}
		~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;

			Hash hash;

			//负载因子到 1 就扩容
			if (_size == _tables.size())//扩容
			{
				size_t newSize = _tables.size() * 2;
				vector<Node*> newTables(newSize, nullptr);
				//这里为了减少调用,不像开散列那样采用复用insert的形式,而是直接将原表中的节点拿下来直接用
				//而且复用insert的时候会涉及空间的申请释放问题(申请新节点,将旧节点的值给新节点,然后释放新旧结点)
				size_t hashi = 0;
				//旧表数据移到新表
				//特别注意:一个一个数据移动,不可一串一串移动,那样的话会造成映射位置错误,最后使其数据不能被正常找到
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->next;
						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* old = _tables[hashi];
			_tables[hashi] = new Node(kv);
			_tables[hashi]->next = old;
			_size++;
			return true;
		}
		Node* Find(const K& key)
		{
			if (_size == 0)
				return nullptr;

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = nullptr;
			for (size_t i = 0; i < _tables.size(); i++)
			{
				cur = _tables[i];
				while (cur)
				{
					if (cur->_kv.first == key)
					{
						return cur;
					}
					cur = cur->next;
				}
			}
			return nullptr;
		}
		void Print()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					cout << "[" << cur->_kv.first << ": " << cur->_kv.second << "]-->";
					cur = cur->next;
				}
			}
			cout << endl;
		}
		bool Erase(const K& key)
		{
			//单链表的删除得知道前一个节点地址,故在这里不在复用Find()函数,否则找不到删除节点的前驱结点
			//Node* ret = Find(key);
			//if (ret)
			//{
			//	Node* next = ret->next;
			//	delete ret;

			//	--_size;
			//	return true;
			//}

			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev)
					{
						prev->next = cur->next;
					}
					else
					{
						_tables[hashi] = cur->next;

					}
					delete cur;
					cur = nullptr;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->next;
				}
			}
			return false;
		}
	private:
		size_t _size = 0;//有效数据个数
		vector<Node*> _tables;
	};

	void testHT1()
	{
		HashTable<int, int> HT;
		int a[] = { 1,7,6,5,2,11,3,43,23,13,54,36 };
		for (auto& e : a)
		{
			HT.Insert(make_pair(e, e));
		}
		HT.Print();

		HT.Erase(13);
		cout << HT.Find(23) << endl;
		cout << HT.Find(13) << endl;
		HT.Print();

		HT.Insert(make_pair(-2, -2));
		HT.Print();
	}
	void testHT2()
	{
		string arr[] = { "梨子","苹果","猕猴桃","桃" ,"梨子","苹果", "猕猴桃","猕猴桃","猕猴桃","梨子","猕猴桃" };
		//HashTable<string, int, HashFuncString> HT;
		HashTable<string, int> HT;
		for (auto& e : arr)
		{
			auto ptr = HT.Find(e);
			if (ptr)
			{
				ptr->_kv.second++;
			}
			else
			{
				HT.Insert(make_pair(e, 1));
			}
		}
		HT.Print();
	}
}

学完不难发现,这里比map和set简单了许多,原来传说中的哈希不过尔尔、哈哈哈哈哈(小小嘚瑟一下);

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值