哈希表及封装

一、介绍

unordered_map unordered_set 的底层就是哈希表。可以关联式容器。

和 map set 的差异:

差异map / setunordered_map / set
key顺序红黑树有序哈希表无序
性能O(logN)O(1),最差O(N)

unordered_map / set 是单向迭代器

会通过哈希函数(直接定址法,除留余数法,平方取中法,随机数法,数字分析法)计算出哈希值,从而确定把数据存在哪个位置,然而计算出的哈希值都会有重复(称为哈希冲突),所以需要有解决哈希冲突的方法(闭散列(线性探测,二次探测),开散列(链地址法),多次散列)

所以由于解决哈希冲突的方法不同,哈希表的实现也不同。下面会实现闭散列的线性探测法和开散列链地址法的哈希表,并用后者封装 unordered_map / set

二、闭散列线性探测

1、实现思路

在一个哈希容器中用哈希函数计算出存储的位置,如果位置有值就冲突,此时向后查找空的位置插入,即我位置被占了就去占别人的位置。

但是为了查找逻辑的自洽,节点必须要标明是空,删除,占据,查找的逻辑是遇到空才停止,如果没有删除,只有空和占据,删除的值会变成空,但是之后的值是可以被查找的,所以要有删除这个特殊标记。

2、代码展示

namespace hashtable
{
	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;
		}
	};

	struct StringHashFunc
	{
		size_t operator()(const string& key)
		{
			size_t hash = 0;
			for (auto ch : key)
			{
				hash *= 131;
				hash += ch;
			}
			return hash;
		}
	};


	// 仿函数解决key->int->哈希值
	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;
			
			// 扩容,负载因子大于0.7就扩容
			if (_size * 10 / _tables.size() >= 7)
			{
				// 不能直接拷贝值,因为扩容大小变了,哈希函数的计算涉及容器大小
				// 导致原有值哈希值旧的和新的是不一样的,所以只能一个一个插
				size_t newsize = _tables.size() * 2;
				HashTable<K, V, Hash> newHT;
				newHT._tables.resize(newsize);
				// 用新哈希表复用哈希表insert
				for (size_t i = 0; i < _tables.size(); i++)
					if (_tables[i]._state == EXIST)
						newHT.insert(_tables[i]._kv);
				 
				_tables.swap(newHT._tables);
			}

			// 计算哈希值,只能模size防止[]访问非法下标
			Hash hs;
			size_t hashi = hs(kv.first) % _tables.size();
			while (_tables[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _tables.size();
			}
			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_size;
			return true;
		}

		HashData<K, V>* find(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			// 找到空就停止的逻辑是:首先计算出了特定位置的哈希值,以这个哈希值开始
			// 找到第一个空就填入,所以在查找时也是先找到哈希值,再向后找就一定是空或找到
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._kv.first == key && _tables[hashi]._state == EXIST)
					return &_tables[hashi];
				++hashi;
				hashi %= _tables.size();
			}
			return nullptr;
		}

		bool erase(const K& key)
		{
			HashData<K, V>* ret = find(key);
			if (ret == nullptr)
				return false;
			else
			{
				ret->_state = DELETE;
				--_size;
				return true;
			}
		}
	private:
		vector<HashData<K, V>> _tables;
		size_t _size = 0;
	};

	/*void TestHT1()
	{
		int a[] = { 10001,11,55,24,19,12,31 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.insert(make_pair(e, e));
		}

		cout << ht.find(55) << endl;
		cout << ht.find(31) << endl;

		ht.erase(55);
		cout << ht.find(55) << endl;
		cout << ht.find(31) << endl;
	}*/

	//void TestHT2()
	//{
	//	int a[] = { 10001,11,55,24,19,12,31 };
	//	HashTable<int, int> ht;
	//	for (auto e : a)
	//	{
	//		ht.insert(make_pair(e, e));
	//	}

	//	ht.insert(make_pair(32, 32));
	//	ht.insert(make_pair(32, 32));
	//}

	//struct Person
	//{
	//	//string _id;

	//	string _name;
	//	int _age;
	//	string school;
	//};


	 key不支持强转整形取模,那么就要自己提供转换成整形仿函数
	void TestHT3()
	{
		// HashTable<Person, int> xxht;

		HashTable<string, int, StringHashFunc> ht;
		//HashTable<string, int> ht;
		ht.insert(make_pair("sort", 1));
		ht.insert(make_pair("left", 1));
		ht.insert(make_pair("insert", 1));

		cout << StringHashFunc()("bacd") << endl;
		cout << StringHashFunc()("abcd") << endl;
		cout << StringHashFunc()("aadd") << endl;
	}

	//void test_map1()
	//{
	//	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
	//"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
	//	unordered_map<string, int> countMap;
	//	for (auto& e : arr)
	//	{
	//		countMap[e]++;
	//	}

	//	cout << countMap.load_factor() << endl;
	//	cout << countMap.max_load_factor() << endl;
	//	cout << countMap.size() << endl;
	//	cout << countMap.bucket_count() << endl;
	//	cout << countMap.max_bucket_count() << endl;

	//	for (auto& kv : countMap)
	//	{
	//		cout << kv.first << ":" << kv.second << endl;
	//	}
	//	cout << endl;
	//}

}

3、细节

(1)何时扩容?

负载因子 = 数据个数 / 容器大小

当负载因子到 0.7 就可以扩容了。

(2)如何计算位置?

用哈希函数计算出一个 size_t 的下标作为位置。

函数的参数也是 size_t ,所以 key 如果类型是 size_t 就可以直接使用哈希函数。

但是有许多时候 key 类型不是 size_t,这时候就要用到仿函数作模板参数,在实例化哈希表时传入仿函数告诉哈希表怎么把 key 转化成哈希函数的参数,进而使用哈希函数得到位置。

可以强转的:

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

使用最多的 string 类型:

// 特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto ch : key)
		{
			hash *= 131;
			hash += ch;
		}
		return hash;
	}
};

(3)性能缺陷

在扩容时哈希表要重新插入数据,会导致性能有一次陡增。

三、开散列链地址法

1、实现思路

冲突的值用链表挂起来,即我位置被人占了我就抢回去。

2、代码展示

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

	template<class K, class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10, nullptr);
			_n = 0;
		}

		bool Insert(const pair<K, V>& kv)
		{
			// 扩容
			// 负载因子为1时扩容
			if (_n == _tables.size())
			{
				size_t newsize = _tables.size() * 2;
                vector<Node*> newtables(newsize, nullptr);
                for (size_t i = 0; i < _tables.size(); i++)
                {
				    Node* cur = _tables[i];
				    while (cur)
				    {
					    Node* next = cur->_next;
					    size_t hashi = cur->_kv.first % newtables.size();
					    cur->_next = newtables[hashi];
					    newtables[hashi] = cur;
					    cur = next;
				    }
				    // 旧表要置空,防止指向两个节点
				    _tables[i] = nullptr;
                }
                _tables.swap(newtables);
			}

			size_t hashi = 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 = kv.first % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}

				cur = cur->_next;
			}

			return nullptr;
		}

	private:
		vector<Node*> _tables; // 指针数组
		size_t _n;

		//vector<list<pair<K, V>>> _tables;
	};
}

3、细节

(1)何时扩容?

规定负载因子等于1就扩容。

(2)如何计算位置?

和上一个一样,只不过代码里面没有写,直接用的 kv.first

四、封装 unordered_map / set

实现思路

(1)节点 HashNode

和库里面一样,节点为了能存储上层容器不同的数据类型(不论是 unordered_map 还是 unordered_set)模板参数是数据类型 T:

template<class T>
struct HashNode
{
	// pair<K, V> _kv;
	T _data;
	HashNode<T>* _next;
	HashNode(const T& data)
		:_data(data)
		,_next(nullptr)
		{ }
};

(2)迭代器 __HtIterator

封装 HashNode 进行哈希表的遍历操作。

迭代器的模板参数:

template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>

Ref 和 Ptr 是为了实现 const 迭代器指向的数据不能修改,从而定义的返回值模板参数。

KeyOfT 是未来封装的两个容器各自告诉哈希表如何取出 T 中的 key 的仿函数。

Hash 就是上面说的如何把类型不是 size_t 的 key 转化成 size_t 以供哈希函数计算出位置。

// 迭代器里面要用哈希表,所以前置声明以免找不到
template<class K, class T, class KeyOfT, class Hash>
class HashTable;

template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HtIterator
{
	typedef HashNode<T> Node;
	typedef __HtIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;


	Node* _node;
	// 实现了两个迭代器,权限只能缩小不能放大,并且在迭代器里面不用修改
	// 所以非const迭代器会权限缩小,const迭代器正常构造
	const HashTable<K, T, KeyOfT, Hash>* _pht;
	

	__HtIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht)
		:_node(node)
		,_pht(pht)
	{ }

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}


	// 只有单向迭代器
	Self& operator++()
	{
		// 当前桶没走完,取下一个节点
		if (_node->_next)
			_node = _node->_next;
		// 当前桶走完了,找第一个不为空的链表的头节点
		else
		{
			KeyOfT kot;
			Hash hs;
			size_t i = hs(kot(_node->_data)) % _pht->_tables.size();
			++i;
			for (; i < _pht->_tables.size(); i++)
			{
				if (_pht->_tables[i])
					break;
			}
			if (i == _pht->_tables.size())
				_node = nullptr;
			else 
				_node = _pht->_tables[i];
		}
		return *this;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}


};

由于迭代器要遍历哈希表,而哈希表正是下面要实现的类,所以首先迭代器内部要找到哈希表,所以构造迭代器时不仅传入对应 HashNode 还要传入对应哈希表的指针,所以需要前置声明 HashTable

之后值得注意的是如何计算位置:

定义仿函数 KeyOfT kot; Hash hs;

先 kot(data) 找到数据 data 里面存的 key

再 hs(key) 把 key 的类型转成 size_t 

最后放到哈希函数计算

最后注意哈希表是单向迭代器,只有++

(3)哈希表 HashTable

哈希表要做的就是封装迭代器供上层容器使用,还有实现上层容器需要的插入删除查找等操作。

所以模板参数:

template<class K, class T, class KeyOfT, class Hash>
	// KeyOfT用于取出unordered_map/set的key值,Hash用于把key值变成哈希值
	template<class K, class T, class KeyOfT, class Hash>
	class HashTable
	{
		typedef HashNode<T> Node;
	public:
		template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
		friend struct __HtIterator; // 定义友元迭代器访问哈希表成员

		typedef __HtIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
		typedef __HtIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;


		iterator begin()
		{
			// 找到哈希表中第一个不为空的链表头节点
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
					return iterator(cur, this); // 除了节点构造,还要传哈希表指针
			}
			return end();
		}

		iterator end()
		{
			return iterator(nullptr, this);
		}

		const_iterator begin() const
		{
			// 找到哈希表中第一个不为空的链表头节点
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				if (cur)
					return const_iterator(cur, this); // 除了节点构造,还要传哈希表指针
			}
			return end();
		}

		const_iterator end() const
		{
			return const_iterator(nullptr, this);
		}






		HashTable()
		{
			_tables.resize(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;
			}
		}

		pair<iterator, bool> insert(const T& data)
		{
			KeyOfT kot;
			// 不允许冗余
			iterator it = find(kot(data));
			if (it != end())
				return {it, false};

			// 扩容,负载因子是1
			Hash hs;
			if (_size == _tables.size())
			{
				size_t newsize = _tables.size() * 2;
				vector<Node*> newtables(newsize, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hs(kot(cur->_data)) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
					// 旧表要置空,防止指向两个节点
					_tables[i] = nullptr;
				}
				_tables.swap(newtables);
			}

			size_t hashi = hs(kot(data)) % _tables.size();
			// 头插
			Node* newnode = new Node(data);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_size;
			return { {newnode, this}, true };
		}


		iterator find(const K& key)
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
					return { cur, this };
				cur = cur->_next;
			}
			return end();
		}

		bool erase(const K& key)
		{
			Hash hs;
			KeyOfT kot;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
						_tables[hashi] = cur->_next;
					else
						prev->_next = cur->_next;
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

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















	/*void TestHT1()
	{
		int a[] = { 10001,11,55,24,19,12,31,4,34,44};
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.insert(make_pair(e, e));
		}

		ht.insert(make_pair(32, 32));
		ht.insert(make_pair(32, 32));
 		ht.erase(31);
		ht.erase(11);
	}*/

	/*void TestHT2()
	{
		HashTable<string, int> ht;
		ht.insert(make_pair("sort", 1));
		ht.insert(make_pair("left", 1));
		ht.insert(make_pair("insert", 1));
	}*/
}

(4)封装 unordered_set

上层容器要做的就是实现取出 key 的仿函数和封装一个哈希表调用哈希表实现的插入删除查找函数。

模板参数带 Hash 是因为要实例化哈希表。

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
	typedef typename hashbucket::HashTable<K, const K, SetKeyOfT, Hash>::iterator iterator;
	typedef typename hashbucket::HashTable<K, const K, SetKeyOfT, Hash>::const_iterator const_iterator;


	iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator begin() const
	{
		return _ht.begin();
	}

	const_iterator end() const
	{
		return _ht.end();
	}

	pair<iterator, bool> insert(const K& key)
	{
		return _ht.insert(key);
	}
private:
	hashbucket::HashTable <K, const K, SetKeyOfT, Hash> _ht;
};

//void Func(const unordered_set<int>& s)
//{
//	unordered_set<int>::iterator it = s.begin();
//	while (it != s.end())
//	{
//		//*it = 1;
//		cout << *it << " ";
//		++it;
//	}
//	cout << endl;
//}

void test_unordered_set()
{
	unordered_set<int> s;
	s.insert(31);
	s.insert(11);
	s.insert(5);
	s.insert(15);
	s.insert(25);

	unordered_set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		//*it = 1;
		cout << *it << " ";
		++it;
	}
	cout << endl;

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

(5)封装 unordered_map

要做的和 unordered_set 一样。

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
public:
	typedef typename hashbucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::iterator iterator;
	typedef typename hashbucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::const_iterator const_iterator;

	iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}

	const_iterator begin() const
	{
		return _ht.begin();
	}

	const_iterator end() const
	{
		return _ht.end();
	}

	pair<iterator, bool> insert(const pair<K, V>& kv)
	{
		return _ht.insert(kv);
	}

	V& operator[](const K& key)
	{
		pair<iterator, bool> ret = insert({key, V()});
		return ret.first->second;
	}
private:
	hashbucket::HashTable <K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};






void test_unordered_map()
{
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };
	unordered_map<string, int> countMap;
	for (auto& e : arr)
	{
		countMap[e]++;
	}

	unordered_map<string, int>::iterator it = countMap.begin();
	while (it != countMap.end())
	{
		//it->first += 'x'; // key不能修改
		it->second += 1;  // value可以修改
		cout << it->first << ":" << it->second << endl;
		++it;
	}
	cout << endl;

	for (auto& kv : countMap)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值