【C++】哈希和哈希封装unordered_map、unordered_set
文章目录
- 前言
- 一. 哈希表的实现
- 1.1 哈希的概念
- 1.2 直接定址法
- 1.3 哈希冲突
- 1.4 负载因子
- 1.5 哈希函数
- 1.5.1 除法散列法/除留余数法
- 1.5.2 乘法散列表(了解)
- 1.5.3 全域散列法(了解)
- 1.6 处理哈希冲突
- 1.6.1 开放定址法
- 线性探测
- 二次探测
- 双重探测(了解)
- 1.6.2 开放定址法的实现
- Insert
- 1.6.3 链地址法及其实现
- Insert
- Find
- Erase
- 构造函数析构函数
- 二. 用哈希表封装unordered_map和unordered_set
- 2.1 源码及框架分析
- 2.2 模拟实现unordered_map和unordered_set
- 2.2.1 复用哈希表的框架,并支持insert
- 2.2.2 迭代器iterator的实现
- 2.2.3 实现
unordered_map
的operator[]
- 2.2.4 构造函数和赋值运算符重载等
- 2.3 源代码
- 最后
前言
在上一篇文章中,我们详细介绍了使用红黑树封装map和set的内容,那么本篇文章将带大家详细讲解哈希表的实现和使用哈希表封装unordered_map和unordered_set,接下来一起看看吧!
一. 哈希表的实现
1.1 哈希的概念
哈希(hash)又称散列,是一种组织数据的方式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字Key跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找。
1.2 直接定址法
当关键字的范围比较集中时,直接定址法就是非常简单高效的方法,比如一组关键字都在[0,99]之间,那么我们开一个100个数的数组,每个关键字的值直接就是存储位置的下标。再比如一组关键字值都在[a,z]的小写字母,那么我们开一个26个数的数组,每个关键字acsii码-a ascii码就是存储位置的下标。也就是说直接定址法本质就是用关键字计算出一个绝对位置或者相对位置。
1.3 哈希冲突
直接定址法的缺点也非常明显,当关键字的范围比较分散时,就很浪费内存甚至内存不够用。假设我们只有数据范围是[0, 9999]的N个值,我们要映射到一个M个空间的数组中(一般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这里要注意的是h(key)计算出的值必须在[0, M)之间。
这里还存在的一个问题就是,
两个不同的key可能会映射到同一个位置去
,这种问题我们叫做哈希冲突
,或者哈希碰撞
。理想情况是找出一个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。
1.4 负载因子
假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么负载因子 = N/M ,负载因子有些地方也翻译为载荷因子/装载因子等,他的英文为load factor。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。
1.5 哈希函数
1.5.1 除法散列法/除留余数法
- 除法散列法也叫做除留余数法,顾名思义,假设哈希表的大小为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:
h(key) = key % M
。 当使用除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等
。如果是 2^X,那么key % 2^X,本质相当于保留key的后X位,那么后X位相同的值,计算出的哈希值都是一样的,就冲突了。如:{63 , 31}看起来没有关联的值,如果M是16,也就是 2^4,那么计算出的哈希值都是15,因为63的二进制后8位是 00111111,31的二进制后8位是 00011111。如果是 10^X,就更明显了,保留的都是10进值的后x位,如:{112, 12312},如果M是100,也就是 ,那么计算出的哈希值都是12。Java的HashMap采用除法散列法时就是2的整数次幂做哈希表的大小M,这样的话,就不用取模,而可以直接位运算,相对而言位运算比模更高效一些
。但是他不是单纯的去取模,比如M是2^16次方,本质是取后16位,那么用key’ = key>>16,然后把key和key’ 异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围内,但是尽量让key所有的位都参与计算,这样映射出的哈希值更均匀一些。
注意:当使用除法散列法时,建议M取不太接近2的整数次幂
的一个质数
(素数)。
1.5.2 乘法散列表(了解)
- 乘法散列法对哈希表大小M没有要求,他的大思路第一步:用关键字 K 乘上常数 A (0<A<1),并抽取出 k * A 的小数部分。第二步:后再用M乘以k * A 的小数部分,再向下取整。
h(key) = floor(M × ((A × key)%1.0))
,其中floor表示对表达式进行下取整,A∈(0,1),这里最重要的是A的值应该如何设定,Knuth认为 0.6180339887…(黄金分割点)比较好。- 乘法散列法对哈希表大小M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A * key = 762.6539420558,取小数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =669.6366651392,那么h(1234) = 669。
1.5.3 全域散列法(了解)
- 如果存在一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。这种方法叫做全域散列。
hab (key) = ((a × key + b)%P )%M
,P需要选一个足够大的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了一个P*(P-1)组全域散列函数组。假设P=17,M=6,a = 3, b = 4, 则 h34 (8) = ((3 × 8 + 4)%17)%6 = 5。- 需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的一个散列函数使用,后续增删查改都固定使用这个散列函数,否则每次哈希都是随机选一个散列函数,那么插入是一个散列函数,查找又是另一个散列函数,就会导致找不到插入的key了。
1.6 处理哈希冲突
实践中哈希表一般还是选择除法散列法作为哈希函数,当然哈希表无论选择什么哈希函数也避免不了冲突,那么在插入数据时,如何解决冲突呢?主要有两种方法:开放定址法和链地址法。
1.6.1 开放定址法
在开放定址法中所有的元素都放到哈希表里,当一个关键字key用哈希函数计算出的位置冲突了,就按照某种规则找到一个没有存储数据的位置进行存储,开放定址法中负载因子一定是小于1的。这里的规则有三种:线性探测、二次探测、双重探测。
线性探测
- 从发生冲突的位置开始,依次线性向后探测,直到寻找到下一个没有存储数据的位置为止(如果走到哈希表尾,则回绕到哈希表头的位置)。
h(key) = hash0 = key % M
,hash0位置冲突了,则线性探测公式为:hc(key,i) = hashi = (hash0 + i) % M, i = {1, 2, 3, …, M − 1}
,因为负载因子小于1,则最多探测M-1次,一定能找到一个存储key的位置。- 线性探测的比较简单且容易实现,线性探测的问题假设:hash0位置连续冲突,hash0,hash1,hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做
群集/堆积
。下面的二次探测可以一定程度改善这个问题。
二次探测
- 从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止(如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置)。
h(key) = hash0 = key % M
,hash0位置冲突了,则二次探测公式为:hc(key,i) = hashi = (hash0 ± i^2 ) % M, i = {1, 2, 3, …, M/2}
- 二次探测当 hashi = (hash0 − i^2)%M 时,当hashi<0时,需要hashi += M
双重探测(了解)
- 第一个哈希函数计算出的值发生冲突,使用第二个哈希函数计算出一个跟key相关的偏移量值,不断往后探测,直到寻找到下一个没有存储数据的位置为止。
h1 (key) = hash0 = key % M
,hash0位置冲突了,则双重探测公式为:hc(key,i) = hashi = (hash0 + i ∗ h2 (key)) % M, i = {1, 2, 3, …, M}
- 要求 h2 (key) < M 且 h2 (key)和M互为质数,有两种简单的取值方法:1、当M为2整数幂时,
h2 (key)从[0,M-1]任选一个奇数
;2、当M为质数时,h2 (key) = key % (M − 1) + 1
1.6.2 开放定址法的实现
这里简单实现一下开放定址法,采用线性探测
来解决哈希冲突即可。
哈希表结构:
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
{
private:vector<HashDate<K, V>> _tables;size_t _n = 0; //表中存储数据的个数
};
这里看一下哈希表的结构:
HashTable
包含了一个vector
数组来存储数据,_n
表示存储数据的个数;HashDate
表示vector
存储数据的类型,里面包含了pair<K,V>
表示存储的数据是key/value
结构,_state
表示当前位置的状态(枚举类型,有EXIST(存在)
,EMPTY(空)
,DELETE(删除)
)。
Insert
首先用除留余数法找到key映射到哈希表的位置(用hash0记录下来),再进行冲突探测,如果哈希冲突了就使用线性探测找到下一个可以存储数据的空位置。这里要考虑两个问题:扩容和key不能取模。
扩容
这里我们哈希表负载因子控制在0.7,当负载因子到0.7以后就需要扩容了,如果我们还是按照2倍扩容,但同时要保持哈希表的大小是一个质数,第一个数是质数,乘2后就不是质数了。那么如何解决这个问题?可以使用sgi版本的哈希表使用的方法,给了一个近似2倍的质数表,每次去质数表获取扩容后的大小即可。
inline unsigned long __stl_next_prime(unsigned long n)
{// Note: assumes long is at least 32 bits.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};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list +__stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;
}
key不能取模的问题
当key是string/Date等类型时,key不能取模,那么我们就需要给哈希表增加一个仿函数,这个仿函数支持把key转换成一个可以取模的整形,如果key可以转换为整型并且不容易冲突,那么这个仿函数返回默认参数即可,如果这个key不能转换为整型,我们就需要自己实现一个仿函数传给这个参数,实现这个仿函数的要求就是尽量让key的每个值都参与到计算中,让不同的key转换出的整型值不同。string做哈希表的key非常常见,所以我们可以考虑把string特化一下。
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){// 字符串转换成整形,可以把字符ascii码相加即可// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的// 这里我们使用BKDR哈希的思路,用上次的计算结果去乘以一个质数,这个质数一般去31, 131等效果会比较好size_t hash = 0;for (auto& e : key){hash *= 131;hash += e;}return hash;}
};
代码实现:
我们可以创建一个HashTable,并且调整它的 _tables 的长度,并且复用Insert插入函数将数据插入到新表中,最后再交换新表和旧表,这样就很简便。
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first)) {return false;}if ((double)_n / (double)_tables.size() >= 0.7){//扩容HashTable<K, V> newTH;newTH._tables.resize(__stl_next_prime(_tables.size() + 1));//将旧表的数据拷贝到新表for (int i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newTH.Insert(_tables[i]._kv);}}_tables.swap(newTH._tables);}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();size_t i = 1;size_t hashi = hash0;while (_tables[hashi]._state == EXIST){// 冲突探测hashi = (hash0 + i) % _tables.size();++i;}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;++_n;return true;
}
Find
首先用除留余数法找到key映射到哈希表的位置(用hash0记录下来),把hash0赋值给hashi,如果hashi这个位置的数据存在且key值相等则返回数据节点的指针,否则进行线性探测:
hashi = (hash0 + i) % _tables.size(),同时i++
,再进行以上的判断,如果遇到空节点则返回nullptr,遇到删除节点则继续进行探测,遇到key值相等的节点则返回节点指针。
HashDate<K, V>* Find(const K& key)
{Hash hs;size_t hash0 = hs(key) % _tables.size();size_t i = 1;size_t hashi = hash0;while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &_tables[i];}hashi = (hash0 + i) % _tables.size();++i;}return nullptr;
}
Erase
Erase可以直接复用Find函数,找到对应的节点,如果没找到则返回false,找到了则将该数据节点的状态设置为
DELETE
即可,最后不要忘了将有效数据个数 _n 减一,返回true。
bool Erase(const K& key)
{HashDate<K, V>* ret = Find(key);if (ret == nullptr) {return false;}else {ret->_state = DELETE;--_n;return true;}
}
注意:在最开始插入数据时要先将哈希表的空间开好,否则调用Find查找数据时,使用除留余数法找到hash0位置时会报错:hash0 = hs(key) % _tables.size()
,因为_tables.size()为0,编译器因为整除0从而报错,所以在构造哈希表时就要对 _tables 开好空间,可以在哈希表构造函数对 _tables 进行调整长度resize。
HashTable()
{_tables.resize(__stl_next_prime(0));
}
1.6.3 链地址法及其实现
解决冲突的思路
开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,链地址法也叫做拉链法
或者哈希桶
。
比如将 {19,30,5,36,13,20,21,12,24,96} 等这一组值映射到M=11的哈希表中:
h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,h(12) = 1,h(24) = 2,h(96) = 88。
哈希表结构:
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
{
private:vector<Node*> _tables;size_t _n = 0;
};
HashTable存储一个vector数组存储节点的指针和有效数据个数 _n,HashNode存储 pair<K,V> 类型的数据和指向下一个节点的指针 _next。
扩容
开放定址法负载因子必须小于1,链地址法的负载因子就没有限制了,可以大于1。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。
Insert
这里就不能像开放定址法那样去搞,得开辟一个新表,再将旧表里的数据移到新表(连带着下面链表的节点一起移动新表),最后再交换旧表和新表。
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))return false;Hash hs;if (_n == _tables.size()){//扩容size_t newsize = __stl_next_prime(_tables.size() + 1);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(cur->_kv.first) % newtables.size();cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = hs(kv.first) % _tables.size();Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}
Find
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;}else{cur = cur->_next;}}return nullptr;
}
Erase
这里的删除就比较繁琐一点,因为删除节点可能影响上一个节点的指向,所以遍历的过程中还需要保存prev节点,并且修改前驱节点prev的指向。
bool Erase(const K& key)
{Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){Node* next = cur->_next;if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = next;}else{prev->_next = next;}delete cur;--_n;return true;}prev = cur;cur = next;}return false;
}
构造函数析构函数
构造函数还是先对_tables进行调整长度即可。编译器虽然会自动调用vector的析构函数,但是vector存储的是指针,无法对指针指向的空间进行释放,需要我们自己写析构函数,手动释放。
HashTable()
{_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;}
}
哈希表的开放定址法和链地址法的完整源代码:https://gitee.com/xie-zhus-shovel/c-learning/tree/master/C%2B%2BLearning/Hash
二. 用哈希表封装unordered_map和unordered_set
2.1 源码及框架分析
SGI-STL30版本源代码中没有unordered_map和unordered_set,SGI-STL30版本是C++11之前的STL版本,这两个容器是C++11之后才更新的。但是SGI-STL30实现了哈希表,只容器的名字是hash_map和hash_set,他是作为非标准的容器出现的,非标准是指非C++标准规定必须实现的,源代码在hash_map/hash_set/stl_hash_map/stl_hash_set/stl_hashtable.h中
hash_map和hash_set的实现结构框架核心部分截取出来如下:
// stl_hash_set
template <class Value, class HashFcn = hash<Value>,
class EqualKey = equal_to<Value>,
class Alloc = alloc>
class hash_set
{
private:typedef hashtable<Value, Value, HashFcn, identity<Value>,EqualKey, Alloc> ht;ht rep;
public:typedef typename ht::key_type key_type;typedef typename ht::value_type value_type;typedef typename ht::hasher hasher;typedef typename ht::key_equal key_equal;typedef typename ht::const_iterator iterator;typedef typename ht::const_iterator const_iterator;hasher hash_funct() const { return rep.hash_funct(); }key_equal key_eq() const { return rep.key_eq(); }
};
// stl_hash_map
template <class Key, class T, class HashFcn = hash<Key>,
class EqualKey = equal_to<Key>,
class Alloc = alloc>
class hash_map
{
private:typedef hashtable<pair<const Key, T>, Key, HashFcn,select1st<pair<const Key, T> >, EqualKey, Alloc> ht;ht rep;
public:typedef typename ht::key_type key_type;typedef T data_type;typedef T mapped_type;typedef typename ht::value_type value_type;typedef typename ht::hasher hasher;typedef typename ht::key_equal key_equal;typedef typename ht::iterator iterator;typedef typename ht::const_iterator const_iterator;
};
// stl_hashtable.h
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey,
class Alloc>
class hashtable {
public:typedef Key key_type;typedef Value value_type;typedef HashFcn hasher;typedef EqualKey key_equal;
private:hasher hash;key_equal equals;ExtractKey get_key;typedef __hashtable_node<Value> node;vector<node*,Alloc> buckets;size_type num_elements;
public:typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey,Alloc> iterator;pair<iterator, bool> insert_unique(const value_type& obj);const_iterator find(const key_type& key) const;
};
template <class Value>
struct __hashtable_node
{__hashtable_node* next;Value val;
};
通过源码可以看到,结构上hash_map和hash_set跟map和set的完全类似,复用同一个hashtable实现key和key/value结构,hash_set传给hash_table的是两个key,hash_map传给hash_table的是pair<const key, value>。
2.2 模拟实现unordered_map和unordered_set
2.2.1 复用哈希表的框架,并支持insert
- 参考源码框架,unordered_map和unordered_set复用之前我们实现的哈希表。
- key参数用K,value参数就用V,哈希表中的数据类型就使用T。
- 其次跟map和set相比而言unordered_map和unordered_set的模拟实现类结构更复杂一点,但是大框架和思路是完全类似的。
- 因为HashTable实现了泛型不知道T参数是K,还是pair<K,V>,那么insert内部进行插入时要用K对象转换成整型取模和K比较相等,因为pair的value不参与计算取模,并且默认支持的是key和value一起比较相等,我们任何时候都只需要比较K对象,所以在unordered_map和unordered_set分别实现一个MapKeyOfT和SetKeyOfT的仿函数传给HashTable的KeyOfT,然后HashTable通过KeyOfT仿函数取出T类型对象中的K对象,再转换成整型取模和K比较相等。
unordered_set.h
namespace My_unordered_set
{template<class K, class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:bool insert(const K& key){return _ht.Insert(key);}private:hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;};
}
unordered_map.h
namespace My_unordered_map
{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:bool insert(const pair<K, V>& kv){return _ht.Insert(kv);}private:hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash> _ht;};
}
HashBucket.h
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 *= 131;hash += e;}return hash;}
};
namespace hash_bucket
{template<class T>struct HashNode{T _data;HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};template<class K, class T, class KeyOfT, class Hash>class HashTable{typedef HashNode<T> Node;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};const unsigned long* first = __stl_prime_list;const unsigned long* last = __stl_prime_list + __stl_num_primes;const unsigned long* pos = lower_bound(first, last, n);return pos == last ? *(last - 1) : *pos;}public:HashTable(){_tables.resize(__stl_next_prime(_tables.size()), 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 T& data){KeyOfT kot;if (Find(kot(data)))return false;Hash hs;size_t hashi = hs(kot(data)) % _tables.size();// 负载因⼦==1扩容if (_n == _tables.size()){vector<Node*> newtables(__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 = hs(kot(cur->_data)) % newtables.size();// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}// 头插Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}private:vector<Node*> _tables; // 指针数组size_t _n = 0; // 表中存储数据个数};
}
2.2.2 迭代器iterator的实现
iterator源代码
template <class Value, class Key, class HashFcn,class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> hashtable;typedef __hashtable_iterator<Value, Key, HashFcn,ExtractKey, EqualKey, Alloc> iterator;typedef __hashtable_const_iterator<Value, Key, HashFcn,ExtractKey, EqualKey, Alloc> const_iterator;typedef __hashtable_node<Value> node;typedef forward_iterator_tag iterator_category;typedef Value value_type;node* cur;hashtable* ht;__hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}__hashtable_iterator() {}reference operator*() const { return cur->val; }#ifndef __SGI_STL_NO_ARROW_OPERATORpointer operator->() const { return &(operator*()); }#endif /* __SGI_STL_NO_ARROW_OPERATOR */iterator& operator++();iterator operator++(int);bool operator==(const iterator& it) const { return cur == it.cur; }bool operator!=(const iterator& it) const { return cur != it.cur; }
};
template <class V, class K, class HF, class ExK, class EqK, class A>__hashtable_iterator<V, K, HF, ExK, EqK, A>&__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{const node* old = cur;cur = cur->next;if (!cur) {size_type bucket = ht->bkt_num(old->val);while (!cur && ++bucket < ht->buckets.size())cur = ht->buckets[bucket];}return *this;
}
迭代器思路分析:
- iterator实现的大框架跟list的iterator思路是一致的,用一个类型封装结点的指针,再通过重载运算符实现,迭代器像指针一样访问的行为,要注意的是哈希表的迭代器是单向迭代器。
- 这里的难点的是operator++的实现。iterator中有一个指向节点的指针,如果当前桶下面还有节点,则节点的指针指向下一个节点即可。如果当前桶走完了,则需要想办法计算找到下一个桶(非空)。那么怎么找到下一个桶呢?参考上面的源码,我们可以看到iterator中除了有节点的指针,还有哈希表对象的指针,这样当前桶走完了,要计算下一个桶就相对容易多了,用key值计算当前桶的位置,依次往后找下一个不为空的桶即可。
- begin()返回第一个桶中第一个节点指针构造的迭代器,这里end()返回迭代器可以用空表示。
- unordered_set的iterator也不支持修改,我们把unordered_set的第二个模板参数改成const K即可, HashTable<K,
const K
, SetKeyOfT, Hash> _ht; - unordered_map的iterator不支持修改key但是可以修改value,我们把unordered_map的第二个模板参数pair的第一个参数改成const K即可, HashTable<K, ,
pair<const K, V>
,
MapKeyOfT, Hash> _ht;
注意:在HashTable
前面定义__HashTable_Iterator
(HashTable
内部要用到迭代器),那么迭代器__HashTable_Iterator
内部怎么访问HashTable
呢?HashTable
的_tables
是私有成员,迭代器__HashTable_Iterator
怎么访问HashTable
的私有成员呢?
首先我们要对
HashTable
进行前置声明,让__HashTable_Iterator
知道有HashTable
这个类。
然后就让__HashTable_Iterator
成为HashTable
的友元。
HashBucket.h
// 前置声明
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 __HashTable_Iterator
{typedef HashNode<T> Node;typedef __HashTable_Iterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;KeyOfT kot;Hash hs;__HashTable_Iterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* ht):_node(node),_ht(ht){ }Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}bool operator==(const Self& it){return _node == it._node;}bool operator!=(const Self& it){return _node != it._node;}Self& operator++(){if (_node->_next){_node = _node->_next;}else{size_t hashi = hs(kot(_node->_data)) % _ht->_tables.size();++hashi;while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){break;}++hashi;}if (hashi == _ht->_tables.size()) _node = nullptr;else _node = _ht->_tables[hashi];}return *this;}Self operator++(int){Self tmp = *this;++(*this);return tmp;}Node* _node;const HashTable<K, T, KeyOfT, Hash>* _ht;
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
public:// 友元声明template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct __HashTable_Iterator;typedef HashNode<T> Node;typedef __HashTable_Iterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef __HashTable_Iterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;Iterator Begin(){for (int i = 0; i < _tables.size(); i++){if (_tables[i]){return Iterator(_tables[i], this);}}return End();}Iterator End(){return Iterator(nullptr, this);}ConstIterator Begin() const{for (int i = 0; i < _tables.size(); i++){if (_tables[i]){return ConstIterator(_tables[i], this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}
private:vector<Node*> _tables;size_t _n = 0;
};
unordered_set迭代器
namespace My_unordered_set
{template<class K, class Hash = HashFunc<K>>class unordered_set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename Hash_Bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;typedef typename Hash_Bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator 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();}private:Hash_Bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;};
}
unordered_map迭代器
namespace My_unordered_map
{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 Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;typedef typename Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator 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();}private:Hash_Bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;};
}
测试:
void test1_set()
{unordered_set<int> st;st.insert(1);st.insert(2);st.insert(3);st.insert(54);st.insert(55);unordered_set<int>::const_iterator it = st.begin();while (it != st.end()){cout << *it << " ";++it;}cout << endl;/*for (auto& e : st){cout << e << " ";}cout << endl;*/
}
void test1_map()
{//const unordered_map<int, int> mp;unordered_map<int, int> mp;mp.insert({ 1,1 });mp.insert({ 2,2 });mp.insert({ 3,4 });mp.insert({ 54,54 });for (auto& e : mp){cout << e.first << ":" << e.second << endl;}
}
2.2.3 实现unordered_map
的operator[]
- unordered_map要支持[]主要需要修改insert返回值支持,修改HashTable中的insert返回值为
pair<Iterator, bool> Insert(const T& data)
。 - 有了insert支持[]实现就很简单了。
unordered_map的[]和map的[]功能一样,如果key值存在,则返回value的引用,如果key值不存在,则先插入key和value的缺省值,再返回value的引用。
HashTable的Insert和Find:
pair<Iterator,bool> Insert(const T& data)
{Iterator it = Find(kot(data));if (it != End())return { it,false };Hash hs;if (_n == _tables.size()){//扩容size_t newsize = __stl_next_prime(_tables.size() + 1);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;++_n;return { Iterator(newnode,this),true };
}
Iterator Find(const K& key)
{Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){return Iterator(cur,this);}else{cur = cur->_next;}}return Iterator(nullptr, this);
}
unordered_set的insert:
pair<iterator,bool> insert(const K& key)
{return _ht.Insert(key);
}
unordered_map的insert和operator[]
:
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;
}
测试:
void test2_map()
{unordered_map<string, string> dict;dict.insert({ "sort", "排序" });dict.insert({ "left", "左边" });dict.insert({ "right", "右边" });dict["left"] = "左边,剩余";dict["insert"] = "插入";dict["string"];unordered_map<string, string>::iterator it = dict.begin();while (it != dict.end()){// 不能修改first,可以修改second//it->first += 'x';it->second += 'x';cout << it->first << ":" << it->second << endl;++it;}cout << endl;
}
2.2.4 构造函数和赋值运算符重载等
HashBucket.h
HashTable(const HashTable& x)
{_tables.resize(__stl_next_prime(0));ConstIterator it = x.Begin();while (it != x.End()){Insert(*it);++it;}
}
HashTable& operator=(HashTable x)
{_tables.swap(x._tables);_n = x._n;return *this;
}
unordered_set.h
unordered_set() = default;
unordered_set(initializer_list<K> il)
{for (const auto& e : il){_ht.Insert(e);}
}
template<class InputIterator>
unordered_set(InputIterator first, InputIterator last)
{while (first != last){_ht.Insert(*first);++first;}
}
unordered_set(const unordered_set& x)
{for (const auto& e : x){_ht.Insert(e);}
}
unordered_set& operator=(const unordered_set& x)
{_ht = x._ht;return *this;
}
unordered_map
unordered_map() = default;
unordered_map(initializer_list<pair<K,V>> il)
{for (const auto& e : il){_ht.Insert(e);}
}
template<class InputIterator>
unordered_map(InputIterator first, InputIterator last)
{while (first != last){_ht.Insert(*first);++first;}
}
unordered_map(const unordered_map& x)
{for (const auto& e : x){_ht.Insert(e);}
}
unordered_map& operator=(const unordered_map& x)
{_ht = x._ht;return *this;
}
2.3 源代码
使用哈希表封装实现unordered_map和unordered_set的源码:https://gitee.com/xie-zhus-shovel/c-learning/tree/master/C++Learning/%E4%BD%BF%E7%94%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E5%B0%81%E8%A3%85unordered_map%E5%92%8Cunordered_set
最后
本篇关于使用哈希表封装unordered_map和unordered_set的实现到这里就结束了,需要大家多去敲代码复现,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!