C++之unordered_map/unordered_set模拟实现
文章目录
一、unorder_set,unorder_map的简单介绍
二、unordered_set,unordered_map模拟实现
2.1 hashtable的底层代码
2.1.1 iterator模拟实现
2.1.2 hashtable模拟实现
2.1.3 hashtable底层源代码(全)
2.2 unordered_set模拟实现
2.3 unordered_map模拟实现
前言
在上一节中,我们学习了如何实现一个哈希表,由于数据的不同定址方式,我们的哈希表又分为好几种,有开放定址法的哈希表,有链定址法的哈希表(哈希桶)。而本节我们就要使用链定址法模拟实现两种STL。
一、unorder_set,unorder_map的简单介绍
我们在之前学习了map和set这两种STL,我们知道了这两种STL能够将数据存储起来并有序排列出来,这源于它们的底层结构是红黑树(红黑树是一棵平衡二叉搜索树,它的中序遍历是有序的)。而我们今天将要学习的unordered_set,unordered_map与它们可以说是“堂兄弟”。它们比map,set多了一个前缀单词unordered(无序的),这就使得了它多了一个特性——无序性。也就是说它们两个也可以存储数据,不过它们不能够将数据有序排列出来,其实说到底还得赖它们的底层结构——哈希表,它不能够像红黑树那样对数据按照某种规则进行排列,我们上一节刚刚学习了哈希表,它是通过一种映射关系将数据放到哈希表中。
而今天我们就通过我们刚刚所学习的哈希表来模拟实现unordered_set和unordered_map。
二、unordered_set,unordered_map模拟实现
说到模拟实现,我们得有理有据吧。那么我们可以先来参考一下SGI-STL30版本中实现这两个STL的源代码:
// 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>.虽然它们的实现方式与map和set十分相似,但是它们对于模板参数的命名可以说是乱七八糟的,接下来我们模拟实现的时候,我们规定key参数就用K,value参数就用V,哈希表中的数据类型,我们使用 T。
我们从上面的源代码中可以看到,它们都是通过封装hashtable来实现的。但是它们两个STL,一个模板类如何传递参数呢?这里又使用到了我们之前所说的泛型编程思想。我们在底层hashtable实现的时候,我们将哈希表数据中的数据类型设置为T,然后我们再定义一个仿函数是用来对应哪个STL使用哪个数据类型,因此我们后面将要在unordered_map和unordered_set层分别实现一个MapKeyOfT和SetKeyOfT的仿函数传给 HashTable的KeyOfT,这样就能够解决,hashtable是K,还是pair<K,V>这个问题了。
2.1 hashtable的底层代码
我们在模拟实现unordered_set,unordered_map之前,我们得先把地基打好,也就是将我们的底层结构——hashtabl实现好了,那样我们模拟实现上面两个STL就会事半功倍了。
首先,我们要确定好hashtable的整体框架。我们在定义结点的数据类型时,我们就要将那个数据类型设置为一个泛型类型T。然后我们还要实现一个迭代器(具体代码后面介绍),和一个哈希表类。
template<class T> //我们使用泛型编程,我们由于要对set和map进行封装,而这两个stl的主要区别就是它们的一个数据的类型不同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;//定义一个迭代器的模板类template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>struct __HTIterator{// ....};template<class K, class T, class KeyOfT, class Hash>class HashTable{private:vector<Node*> _tables; // 指针数组size_t _n = 0; // 表中存储数据个数};
2.1.1 iterator模拟实现
迭代器的模拟实现,我们在之前的学习中也多次遇到,我们可以将迭代器看作一个类似于指针的东西(实际上它不是指针),然后我们将其封装成为一个类,我们在那个类中模拟实现它的各种迭代器的功能(*,->,++,!=,==等等)。我们这里实现的时候要注意:hashtable的迭代器是一个单向迭代器。
我们这里实现的难点主要是operator++,我们要明白++是迭代器往后走一步,这里迭代器是在按照结点来走的,但是我们所有的结点并不是在一条链上的,它们是一个个链表挂在哈希表上面的(哈希桶的本质:解决哈希冲突的问题),因此我们迭代器在走的时候,如果当前结点的下面还有结点的话,我们就继续往下走。如果当前的哈希桶走完了,我们就得想办法走到下一个哈希桶中去,这里我们就可以参考上面stl源码中的解决方法了,它定义了一个哈希表对象的指针,这样的话,当当前的桶走完后,我们移动到下一个桶就十分方便了(我们直接使用插入的方法,同时对位置进行移动)。上面是实现迭代器的一个大体思路,下面是实现迭代器的一些细节问题:
- 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;
// 前置声明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 HashTable<K, T, KeyOfT, Hash> HT;typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node;const HT* _ht;__HTIterator(Node* node, const HT* ht):_node(node), _ht(ht){}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 hashi = hs(kot(_node->_data)) % _ht->_tables.size();++hashi;while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}else{++hashi;}}if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}};
如上代码,是迭代器实现代码,我们照着上面的代码进行分析。开始我们要对Hashtable的类进行一个声明,因为在编译器中,它对于对象的访问是从上往下的,由于我们是先来实现迭代器的,上面并没有实现Hashtable,但是我们在operator++中需要定义一个Hashtable对象,那时肯定需要Hashtable的类来进行实例化的。上面那些各种运算符重载,我注重讲一下operator++。
对于这种可能有多种情况,我们优先把简单情况写了,然后复杂的情况放到else中再慢慢进行讨论分析。在第一个if语句中,我们讨论的是我们当前结点的下面还是有结点的情况,于是我们直接将当前结点往后面移动即可,else语句讨论的是我们当前结点后面没有结点了,需要我们寻找下一个桶了。我们先计算当前结点在哈希表中的位置hashi,然后再将它与哈希表的大小进行判断,如果它小于哈希表的大小,就说明它是在哈希表桶内。如果当前的哈希桶中有值的话(这里有值的意思是,在这个位置上哈希表挂着一个哈希桶),我们就将结点移动过去(直接赋值即可),如果没有值的话,我们继续移动在哈希表上的位置。当移动到最后的位置时(_table.size()),我们就结束,将结点置于nullptr。
2.1.2 hashtable模拟实现
这里hashtable的实现代码,和我们上一节实现的代码差不多,基本是在上节内容的基础上进行一些修改。首先我们在上面实现了迭代器,因此我们要在哈希表中将其引入过来,我们直接使用友元类将迭代器类放到我们hashtable类中,然后实现一些迭代器(Begin(),End())。
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct __HTIterator;typedef HashNode<T> Node;public:typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;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);}ConstIterator Begin() const{for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return ConstIterator(cur, this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}
Begin()返回的迭代器就是哈希桶中的第一个结点指针的迭代器,于是我们直接遍历整个哈希表,我们使用条件语句if判断那个结点上有没有值,有值的话就直接返回当前结点的指针和this指针复合的迭代器,没有的话就返回End(),我们将End()设置为iterator(nullptr,this)
对于Find,Erase等操作基本没有什么变化,只有对数据的处理进行了一点修改(我们要确定是哪个类型的值,使用仿函数来进行指认)
Iterator Find(const K& key){Hash hs;size_t hashi = hs(key) % _tables.size();Node* cur = _tables[hashi];KeyOfT kot;while (cur){if (kot(cur->_data) == key){return Iterator(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;}prev = cur;cur = cur->_next;}return false;}
最后,我们来结束hashtable中最麻烦的操作——Insert。我们这里inset的返回值类型是一个pair<iterator,bool>键值对类型,至于其他的操作和我们上节课实现的哈希表的插入操作基本上如出一辙,我们在返回返回值时,注意一下使用{ }来进行隐式类型转换。
pair<Iterator, bool> Insert(const T& data){KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return { it, false };Hash hs;// 负载因子到1就扩容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;// cur头插到新表size_t hashi = hs(kot(cur->_data)) % newSize;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 };}
2.1.3 hashtable底层源代码(全)
namespace hash_bucket
{template<class T> //我们使用泛型编程,我们由于要对set和map进行封装,而这两个stl的主要区别就是它们的一个数据的类型不同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;//定义一个迭代器的模板类template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>struct __HTIterator{typedef HashNode<T> Node;typedef HashTable<K, T, KeyOfT, Hash> HT;typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;Node* _node;const HT* _ht;__HTIterator(Node* node, const HT* ht):_node(node), _ht(ht){}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 hashi = hs(kot(_node->_data)) % _ht->_tables.size();++hashi;while (hashi < _ht->_tables.size()){if (_ht->_tables[hashi]){_node = _ht->_tables[hashi];break;}else{++hashi;}}if (hashi == _ht->_tables.size()){_node = nullptr;}}return *this;}bool operator!=(const Self& s){return _node != s._node;}bool operator==(const Self& s){return _node == s._node;}};// hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT> _ht; ->map// hash_bucket::HashTable<K, K, SetKeyOfT> _ht; ->set//我们定义哈希表的类模板函数时,我们要包含关键字K,以及关键字所对应的值的数据类型,以及我们可以匹配类型的仿函数,//还有一个用于类型转换的仿函数template<class K, class T, class KeyOfT, class Hash>class HashTable{template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>friend struct __HTIterator;typedef HashNode<T> Node;public:typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;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);}ConstIterator Begin() const{for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return ConstIterator(cur, this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}HashTable(){_tables.resize(__stl_next_prime(1), 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;}}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;}pair<Iterator, bool> Insert(const T& data){KeyOfT kot;Iterator it = Find(kot(data));if (it != End())return { it, false };Hash hs;// 负载因子到1就扩容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;// cur头插到新表size_t hashi = hs(kot(cur->_data)) % newSize;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];KeyOfT kot;while (cur){if (kot(cur->_data) == key){return Iterator(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;}prev = cur;cur = cur->_next;}return false;}private://vector<list<pair<K, V>>> _tables;vector<Node*> _tables; // 指针数组size_t _n = 0; // 表中存储数据个数};
}
2.2 unordered_set模拟实现
对于unordered_set的模拟实现,我们基本上都是复用我们上面实现的hashtable的接口。我们得先实现一下unordered_set的SetKeyOfT,这一unordered_set的K,就能够和hashtable的T建立联系了。
namespace hjc
{template<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};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();}pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}private:hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;};
}
我们看上面的代码,可以看到我们在typedef的时候,我们加上了一个typename,这样是为了让编译器知道,它是一个类名,不然编译器可能会将其当作是一个类类型,这点我们需要注意。至于其余的代码,我们直接复用哈希表对象中的接口即可。
2.3 unordered_map模拟实现
同样地,对于它的模拟实现和unordered_set基本差不多,基本都是复用hashtable的。但是其中的一个operator[ ],我们要稍微理解一下(虽然我们在前面已经讲过好几次了)
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 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();}pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}V& operator[](const K& key){pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));return ret.first->second;}private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;};