【数据结构】哈希表(散列表)

avatar
作者
猴君
阅读量:0

目录

1、unordered系列关联式容器

2、哈希概念

3、哈希函数

3.1 直接定址法

3.2 除留余数法

4、哈希冲突

4.1 闭散列(开放定址法)

4.1.1 线性探测

4.1.2 二次探测

4.1.3 线性探测代码实现

插入

搜索

删除

对于不可以取模的类型

4.2 开散列(哈希桶/拉链法)

插入

搜索

删除

对于不可取模的类型


1、unordered系列关联式容器

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

这四个容器是unordered_set、unordered_map、unordered_multiset和unordered_multimap。这四个容器后序会有详细介绍,这里就简单介绍一下unordered_set和set的区别,为了后面更好地介绍哈希表

unoredered_set和set百分之90的内容都是相同的,不同点有:

1. 对key的要求不同

set:支持比较大小(自定义类型自己写比较函数)

unordered_set:支持key转化为整型 + 比较相等

2. set遍历有序,unordered_set遍历无序

3. set是双向迭代器,unordered_set是单向迭代器

4. 性能差异(插入、删除、搜索)

set:O(logN)             unordered_set:O(1)

2、哈希概念

哈希/散列是一种思想,指的是值与值建立映射关系

哈希表/散列表是根据哈希思想设计出来的数据结构,是将值与存储位置建立映射关系

我们使用一道题来引入哈希表

这道题是只包含小写字母的,所以我们可以想到,创建一个大小为26的整型数组,用一个下标代表一个字符,下标 = 字符 - ‘a’,然后遍历一遍字符串s,统计出每个字符出现的个数,完成之后,再遍历一次字符串s,如果遍历到的字符对应的下标的值是1,就代表这个字符再字符串中只出现了1次,也就是结果

class Solution { public:     int firstUniqChar(string s) {         // 统计每个字符出现的次数         int count[26] = {0};         int size = s.size();         for(int i = 0; i < size; ++i)             count[s[i] - 'a'] += 1;         // 按照字符次序从前往后找只出现一次的字符         for(int i = 0; i < size; ++i)             if(1 == count[s[i] - 'a'])                 return i;         return -1;     } };

这就是哈希思想,那个数组就是哈希表。将字符串中可能出现的字符与数组的下标(存储位置)建立起了映射关系。当搜索一个值出现的次数时count[s[i] - 'a'],时间复杂度就是O(1)。但与真正的哈希表也是有些区别,真正的哈希表是将值(在这里也就是字符)放到映射的存储位置,而这里是将值出现的次数放到映射的存储位置。

我们之前学过的顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN),搜索的效率取决于搜索过程中元素的比较次数

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素

当向该结构中:

插入元素:根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

若要将值与存储位置建立映射关系,就需要使用哈希函数

3、哈希函数

将值与存储位置建立关系的函数,称为哈希函数

常用的哈希函数有多种,这里仅介绍两种

3.1 直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

用key的值映射一个绝对位置或相对位置

优点:简单、均匀、没有冲突

缺点:只适合范围集中的数据

使用场景:适合查找比较小且连续的情况
上面的题目使用的就是这种方法,地址 = 字符 - 'a',之所以适用这种方法,是因为26个字母是连着的,是范围集中的数据,加入有一组数据是10,20,15,35,66,16,10000,如果使用直接定址法,需要开一个10000大小的数组,并且里面的数据十分分散,显然是不适用的

3.2 除留余数法

key模表的大小N后,映射到表中的一个位置

hashi = key % N,N是表的大小

假设有一组数据10,20,15,35,66,16,10000,表的大小是10,会发现,用10和20模表的大小,结果都是0,那是将10和20都放到下标为0的位置吗?这种情况称为哈希冲突

4、哈希冲突

哈希冲突就是指不同值,取模以后映射到相同位置,也称为哈希碰撞

解决哈希冲突的两种常用方式是:闭散列和开散列

4.1 闭散列(开放定址法)

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

闭散列就是按规则去其他位置找一个空位置存储,也就是根据这个规则去寻找“下一个”位置,寻找下一个位置的方法有两种,线性探测和二次探测 

4.1.1 线性探测

线性探测就是若映射到的位置已经被占了,则从这个位置开始,往下寻找空的位置,如何将数据放入空位置中。但这种方法容易造成一片拥堵,因为这个值占用了别人的位置,后面插入的值又要往后占位置

当前位置 + 1 ,若还被占了,当前位置+2,...,一直到空位置

4.1.2 二次探测

二次探测与线性探测是类似的,都是当映射到的位置已经被占了,当前位置 + 1^2 ,若还被占了,当前位置 + 2^2,...,一直到空位置

在前面,我们有提到set和unordered_set对key的要求不同, unordered_set要求key可以比较相等,正是在这里体现了这个作用。并且支持将key转为整型就是为了能够取模

在代码实现,我们只实现线性探测,因为两者是类似的

4.1.3 线性探测代码实现

我们前面只是了解了线性探测的插入操作,接下来我们了解一下线性探测的查找和删除操作

查找16:先去16映射到的位置查看,比较这个位置存储的值与16是否相等,若不想等,则继续往后查找,若到了空,则表中没有这个数据。像这里,16映射到的位置存放的是35,不是16,所以继续向后查找,一直到下标为8的时候找到16。假设要查找的是26,则26映射到的位置是35,继续向后查找,会发现,一直到下标为9没有存放值的位置都没有找到26,所以表中没有26

删除66:先查找66,若66存在,则删除

此时如果再查找16,会发现表中有16,但是找不到。所以,表中存数据时,每个位置不应该只存数据,还应该存放这个数据的状态。数据的状态有3种:存在、不存在、删除。此时也需要修改一下上面的查找和删除逻辑。查找除了要比较值是否相同,并且还需要这个值的状态是存在,才算找到,找不到时,是从映射的位置一直向后找,直到状态为空,才算找不到,如果遇到状态是删除的,仍然需要向后找。删除可以不改变这个结点的数据,只需要将这个结点的状态改成删除即可

搜索、插入、删除也不是完全只需要1次,可能需要几次,但仍然是常数时间,所以时间复杂度是O(1)

enum State { 	EXIST, 	EMPTY, 	DELETE }; template<class K,class V> struct HashDate // 哈希表是这个类型的数组 { 	pair<K, V> _kv; 	State _state = EMPTY; // 默认状态为空 }; template<class K,class V> class HashTable { public: 	HashTable() 	{ 		_tables.resize(10); 	} private: 	vector<HashDate<K, V>> _tables; };
插入

首先要注意,取模时,数组的大小需要根据size()计算,不能用capacity来计算,因为是需要将数据放进算出的位置的,若是用capacity,operator[]会报错

bool Insert(const pair<K, V>& kv) { 	size_t hashi = kv.first % _tables.size(); 	// 找到空或删除的位置将数据往里面放 	while (_tables[hashi]._state == EXIST) 	{ 		++hashi; 		hashi %= _tables.size(); // 防止++后超过数组范围 	} 	_tables[hashi]._kv = kv; 	_tables[hashi]._state = EXIST; 	return true; }

此时当哈希表满了时,会出现死循环,所以,需要对哈希表进行扩容操作

哈希表需要在什么时候进行扩容?如何扩容呢?

哈希表的载荷因子定义为;a = 填入表中的数据个数 / 哈希表的长度

a是哈希表装满程度的标志因子,由于表长是定值,a与"填入表中的数据个数"成正比,所以,a越大,表明填入表中的元素越多,产生冲突的可能性越大,反之,a越小,表明填入表中的元素越少,产生冲突的可能性越小。实际上,哈希表的平均查找长度是载荷因子a的函数,只是不同的处理冲突的方法有不同的函数。

对于开放定址法,载荷因子是特别重要的因素,应严格限制载0.7~0.8以下,当载荷因子过大,就要扩容。所以,哈希表永远不会满,当快满时就扩容了,无需担心上面的死循环

扩容时,不能直接对这个vector进行resize,因为扩容后,原来冲突的数据,现在可能不冲突了

10,20,10000原本3个都是冲突的,现在只有20和10000冲突了,66和16也是同理

扩容方法:

方法一:当满了,创建一个vector,然后遍历原来的哈希表,将这些数据插入到新开的vector中,最后再swap两个vector。这种方法不好,因为需要重复写插入函数中的代码

方法二:创建一个哈希表对象,然后遍历原来的哈希表,通过新创建的哈希表对象调用Insert函数,将原来哈希表中的值一个一个插入,这样就能完成代码的复用

bool Insert(const pair<K, V>& kv) { 	// 扩容 	if (_n * 10 / _tables.size() >= 7) 	{ 		HashTable<K, V> newHT; 		newHT._tables.resize(_tables.size() * 2); 		for (size_t i = 0; i < _tables.size(); i++) 		{ 			if (_tables[i]._state == EXIST) 			{ 				newHT.Insert(_tables[i]._kv); 			} 		} 		_tables.swap(newHT._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; 	_n++; 	return true; }
搜索

Find不能只比较key是否相同,因为在删除操作中,删除数据时,只会将其状态修改为删除,并不会将数据真的删除,所以除了比较key相同,还需要状态是存在才是真的找到了

HashDate<K, V>* Find(const K& key) { 	size_t hashi = key % _tables.size(); 	while (_tables[hashi]._state != EMPTY) 	{ 		if (_tables[hashi]._state == EXIST 			&& _tables[hashi]._kv.first == key) 		{ 			return &_tables[hashi]; 		} 		++hashi; 		hashi %= _tables.size(); 	} 	return nullptr; }

这时候我们可以对插入进行修改,使之不能放入重复元素

bool Insert(const pair<K, V>& kv) { 	if (Find(kv.first)) 		return false; 	// 扩容 	if (_n * 10 / _tables.size() >= 7) 	{ 		HashTable<K, V> newHT; 		newHT._tables.resize(_tables.size() * 2); 		for (size_t i = 0; i < _tables.size(); i++) 		{ 			if (_tables[i]._state == EXIST) 			{ 				newHT.Insert(_tables[i]._kv); 			} 		} 		_tables.swap(newHT._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; 	_n++; 	return true; }
删除
bool Erase(const K& key) { 	HashDate<K, V>* ret = Find(key); 	if (ret == nullptr) 		return false; 	ret->_state = DELETE; 	return true; }

不需要写析构、拷贝构造和赋值运算符重载,因为成员变量是vector和内置类型

对于不可以取模的类型

我们可以使用下面的代码对我们上面实现的哈希表进行测试

void test_hash1() { 	HashTable<int, int> hash; 	int a[] = { 10,20,15,35,66,16,10000 }; 	for (auto e : a) 	{ 		hash.Insert({ e,e }); 	} 	hash.Insert({ 9,9 }); 	hash.Insert({ 3,3 }); 	hash.Erase(66); }

此时建立的哈希表的key是整型,是可以取模的。那若是存储的key是string等不可取模的类型,又该怎么办呢?

此时就需要用到仿函数了,对于int、size_t、double、float、long、long long、char等可以直接取模的类型,就使用仿函数的缺省值,对于string等无法取模的类型,就自己写仿函数,然后定义哈希表时传过去

template<class K> struct HashFunc { 	size_t operator()(const K& key) 	{ 		return (size_t)key; 	} }; 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 / _tables.size() >= 7) 		{ 			HashTable<K, V> newHT; 			newHT._tables.resize(_tables.size() * 2); 			for (size_t i = 0; i < _tables.size(); i++) 			{ 				if (_tables[i]._state == EXIST) 				{ 					newHT.Insert(_tables[i]._kv); 				} 			} 			_tables.swap(newHT._tables); 		} 		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; 		_n++; 		return true; 	} 	bool Erase(const K& key) 	{ 		HashDate<K, V>* ret = Find(key); 		if (ret == nullptr) 			return false; 		ret->_state = DELETE; 		return true; 	} 	HashDate<K, V>* Find(const K& key) 	{ 		Hash hs; 		size_t hashi = hs(key) % _tables.size(); 		while (_tables[hashi]._state != EMPTY) 		{ 			if (_tables[hashi]._state == EXIST 				&& _tables[hashi]._kv.first == key) 			{ 				return &_tables[hashi]; 			} 			++hashi; 			hashi %= _tables.size(); 		} 		return nullptr; 	} private: 	vector<HashDate<K, V>> _tables; 	size_t _n = 0; // 表中存储数据个数 };

可以看到,对于可以直接取模的类型,是先转化为size_t,再取模。对于string等不能取模的类型也是类似。此时是完成了两层映射,先用string去映射整型,再用整型去映射出对于的存储位置。注意,在这两层映射中,每一层都可能会产生冲突

那string要如何映射为整型呢?

此时就涉及到了字符串哈希算法。

可不可以取地址,将地址转为整型呢?不可以,因为两个相同的字符串的地址会不同,这样映射成的整型也是不同的

应当将string中的每个字母的ASCII相加,但是这样太容易发生冲突了,因为像abcd和dcba映射为整型时就会产生冲突,为了减少冲突,没加一个值时,可以对目前的和乘一个固定的数

struct StringHashFunc { 	size_t operator()(const string& s) 	{ 		size_t hash = 0; 		for (auto e : s) 		{ 			hash *= 31; 			hash += e; 		} 		return hash; 	} }; void test_hash2() { 	HashTable<string, string, StringHashFunc> hash; 	hash.Insert({ "left","左边" }); 	hash.Insert({ "right","右边" }); }

在我们使用unordered_map时,会发现当存储的key是string时,并不需要自己写仿函数,这是因为string很常用,所以STL底层对string进行了特化。我们实现时可以对其进行模板的特化

namespace open_address // 开放定址法 { 	enum State 	{ 		EXIST, 		EMPTY, 		DELETE 	}; 	template<class K, class V> 	struct HashDate // 哈希表是这个类型的数组 	{ 		pair<K, V> _kv; 		State _state = EMPTY; // 默认状态为空 	}; 	template<class K> 	struct HashFunc 	{ 		size_t operator()(const K& key) 		{ 			return (size_t)key; 		} 	}; 	template<> // 对string类型进行特化 	struct HashFunc<string> 	{ 		size_t operator()(const string& s) 		{ 			size_t hash = 0; 			for (auto e : s) 			{ 				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 / _tables.size() >= 7) 			{ 				HashTable<K, V, Hash> newHT; 				newHT._tables.resize(_tables.size() * 2); 				for (size_t i = 0; i < _tables.size(); i++) 				{ 					if (_tables[i]._state == EXIST) 					{ 						newHT.Insert(_tables[i]._kv); 					} 				} 				_tables.swap(newHT._tables); 			} 			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; 			_n++; 			return true; 		} 		bool Erase(const K& key) 		{ 			HashDate<K, V>* ret = Find(key); 			if (ret == nullptr) 				return false; 			ret->_state = DELETE; 			return true; 		} 		HashDate<K, V>* Find(const K& key) 		{ 			Hash hs; 			size_t hashi = hs(key) % _tables.size(); 			while (_tables[hashi]._state != EMPTY) 			{ 				if (_tables[hashi]._state == EXIST 					&& _tables[hashi]._kv.first == key) 				{ 					return &_tables[hashi]; 				} 				++hashi; 				hashi %= _tables.size(); 			} 			return nullptr; 		} 	private: 		vector<HashDate<K, V>> _tables; 		size_t _n = 0; // 表中存储数据个数 	}; }

实际上,解决哈希冲突的闭散列方式,无论是线性探测还是二次探测都不是太好,因为本质都是一种零和博弈

4.2 开散列(哈希桶/拉链法)

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
上面的数据10,20,15,35,66,16,10000,使用开散列就是

这里的哈希表就是一个指针数组,下面挂着的就是链表,所以结点除了要存储值,还要存储下一个结点的地址。每一个链表称为一个哈希桶

namespace hash_bucket { 	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 HashTable 	{ 		typedef HashNode<K, V> Node; 	public: 		HashTable() 		{ 			_tables.resize(10, nullptr); 		} 	private: 		vector<Node*> _tables; 		size_t _n = 0; // 表中存储数据个数 	}; }
插入

插入首先计算出插入值的插入位置,然后往插入位置的链表中插入该值。因为冲突的这些值的先后顺序是任意的,所以插入一个值时既可以头插,也可以尾插,我们这里采用的是头插

bool Insert(const pair<K, V>& kv) { 	size_t hashi = kv.first % _tables.size(); 	// 头插 	Node* newNode = new Node(kv); 	newNode->_next = _tables[hashi]; 	_tables[hashi] = newNode; 	++_n; 	return true; }

 使用哈希桶扩容时与前面有些不同,是当载荷因子等于1时才进行扩容

bool Insert(const pair<K, V>& kv) { 	size_t hashi = kv.first % _tables.size(); 	// 负载因子==1,扩容 	if (_n == _tables.size()) 	{ 		HashTable<K, V> newHT; 		newHT._tables.resize(_tables.size() * 2); 		for (size_t i = 0; i < _tables.size(); i++) 		{ 			Node* cur = _tables[i]; 			// 遍历整个哈希表,当桶不为空时,让cur遍历这个桶,然后插入,直到cur为空,说明这个桶已经完了 			while (cur) 			{ 				newHT.Insert(cur->_kv); 				cur = cur->_next; 			} 		} 		_tables.swap(newHT._tables); 	} 	// 头插 	Node* newNode = new Node(kv); 	newNode->_next = _tables[hashi]; 	_tables[hashi] = newNode; 	++_n; 	return true; }

在前面,完美看到了哈希表底层就是一个vector<Node*>,是一个指针数组,下面挂着的那些链表是我们new出来的,所以也需要我们自己释放,所以是需要自己实现析构函数的

~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; 	} }

此时的插入操作中的扩容是不好的,因为我们没有利用表中原有的结点,而是重新创建结点,原来的结点还会delete掉,倘若扩容前有10000个结点,那么会进行10000次new和delete,这样消耗是比较大的。所以我们不应该调用Insert,因为一旦调用Insert就一定会有new和delete。此时应该创建一个vector<Node*>,并且复用原来的结点,即用上面提到的方法一

bool Insert(const pair<K, V>& kv) { 	size_t hashi = kv.first % _tables.size(); 	// 负载因子==1,扩容 	if (_n == _tables.size()) 	{ 		vector<Node*> newtables(_tables.size() * 2, 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); 	} 	// 头插 	Node* newNode = new Node(kv); 	newNode->_next = _tables[hashi]; 	_tables[hashi] = newNode; 	++_n; 	return true; }
搜索
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; 	Hash hs; 	size_t hashi = hs(kv.first) % _tables.size(); 	// 负载因子==1,扩容 	if (_n == _tables.size()) 	{ 		vector<Node*> newtables(_tables.size() * 2, nullptr); 		for (size_t i = 0; i < _tables.size(); i++) 		{ 			Node* cur = _tables[i]; 			while (cur) 			{ 				Node* next = cur->_next; 				// 旧表中的结点,挪动到新表重新映射的位置 				size_t hashi = hs(cur->_kv.first) % newtables.size(); 				// 头插到新表 				cur->_next = newtables[hashi]; 				newtables[hashi] = cur; 				cur = next; 			} 			// 原来的桶取完后要置空 			_tables[i] = nullptr; 		} 		_tables.swap(newtables); 	} 	// 头插 	Node* newNode = new Node(kv); 	newNode->_next = _tables[hashi]; 	_tables[hashi] = newNode; 	++_n; 	return true; }
删除

删除操作,记录当前遍历到的结点cur和cur的前一个结点prev,删除时让prev的_next指向cur的下一个即可,然后释放cur,但若cur是头结点需要特殊考虑,因为头结点没有前一个结点,此时直接让表里的指针指向cur的下一个

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 == nullptr) 				_tables[hashi] = cur->_next; 			else 				prev->_next = cur->_next; 			delete cur; 			return true; 		} 		prev = cur; 		--_n; 		cur = cur->_next; 	} 	return false; }
对于不可取模的类型

与开放定址法是类似的

namespace hash_bucket // 哈希桶 { 	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> 	struct HashFunc 	{ 		size_t operator()(const K& key) 		{ 			return (size_t)key; 		} 	}; 	template<> // 对string类型进行特化 	struct HashFunc<string> 	{ 		size_t operator()(const string& s) 		{ 			size_t hash = 0; 			for (auto e : s) 			{ 				hash *= 31; 				hash += e; 			} 			return hash; 		} 	}; 	template<class K, class V, class Hash = HashFunc<K>> 	class HashTable 	{ 		typedef HashNode<K, V> Node; 	public: 		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; 			} 		} 		bool Insert(const pair<K, V>& kv) 		{ 			if (Find(kv.first)) 				return false; 			Hash hs; 			size_t hashi = hs(kv.first) % _tables.size(); 			// 负载因子==1,扩容 			if (_n == _tables.size()) 			{ 				//HashTable<K, V> newHT; 				//newHT._tables.resize(_tables.size() * 2); 				//for (size_t i = 0; i < _tables.size(); i++) 				//{ 				//	Node* cur = _tables[i]; 				//	// 遍历整个哈希表,当桶不为空时,让cur遍历这个桶,然后插入,直到cur为空,说明这个桶已经完了 				//	while (cur) 				//	{ 				//		newHT.Insert(cur->_kv); 				//		cur = cur->_next; 				//	} 				//} 				//_tables.swap(newHT._tables); 				vector<Node*> newtables(_tables.size() * 2, nullptr); 				for (size_t i = 0; i < _tables.size(); i++) 				{ 					Node* cur = _tables[i]; 					while (cur) 					{ 						Node* next = cur->_next; 						// 旧表中的结点,挪动到新表重新映射的位置 						size_t hashi = hs(cur->_kv.first) % newtables.size(); 						// 头插到新表 						cur->_next = newtables[hashi]; 						newtables[hashi] = cur; 						cur = next; 					} 					// 原来的桶取完后要置空 					_tables[i] = nullptr; 				} 				_tables.swap(newtables); 			} 			// 头插 			Node* newNode = new Node(kv); 			newNode->_next = _tables[hashi]; 			_tables[hashi] = newNode; 			++_n; 			return true; 		} 		Node* Find(const K& key) 		{ 			Hash hs; 			size_t hashi = hs(key) % _tables.size(); 			Node* cur = _tables[hashi]; 			while (cur) 			{ 				if (cur->_kv.first == key) 					return cur; 				cur = cur->_next; 			} 			return nullptr; 		} 		bool Erase(const K& key) 		{ 			Hash hs; 			size_t hashi = hs(key) % _tables.size(); 			Node* prev = nullptr; 			Node* cur = _tables[hashi]; 			while (cur) 			{ 				if (cur->_kv.first == key) 				{ 					if (prev == nullptr) 						_tables[hashi] = cur->_next; 					else 						prev->_next = cur->_next; 					delete cur; 					return true; 				} 				prev = cur; 				--_n; 				cur = cur->_next; 			} 			return false; 		} 	private: 		vector<Node*> _tables; 		size_t _n = 0; // 表中存储数据个数 	}; }

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!