【C++】封装unordered_set和unordered_map
目录
前言:
一:修改参数
二:实现迭代器
三:实现++运算符重载
四:其他运算符的重载
五:实现begin和end
六:实现const类型迭代器
七:修改Insert返回值
八:重载map的 [ ]
九:修改HashFunc仿函数
总结:
前言:
我们之前已经讲到过哈希表是如何实现了,相信追剧的小伙伴C++功底已经突飞猛进了,在底层中,不仅有map和set,还有unordered_set和unordered_map,底层正是用我们之前写的哈希表实现的(本篇会跳过很多细节,因为之前已经讲过封装,可以看这篇博客:【C++】通过红黑树封装map和set-CSDN博客)。
这里我们就用之前的哈希表代码,继续在上面封装,将其封装为unordered_set和unordered_map。和红黑树封装一样,内容劲爆,老铁焖做好心理准备,我们开始了!
一:修改参数
还是和之前一样,这里已经做好了它们的头文件,之后需要三个参数,而对于HashTable需要再多加一个模板参数,也就是KeyOfT。KeyOfT的作用就是找是K还是pair的first。我们给出unorderedSet头文件代码:
#pragma once
#include"myHash.h"namespace bit
{template<class K>class unordered_set{public:struct SetOfK{const K& operator()(const K& key){return key;}};//插入函数bool insert(const K& key){return _ht.Insert(key);}//删除函数bool erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K, K, SetOfK> _ht;};
}
map如下:
#include"myHash.h"namespace bit
{template<class K, class V>class unordered_map{public:struct MapOfK{const K& operator()(const pair<K, V>& kv){return kv.first;}};//插入函数bool insert(const pair<K, V>& kv){return _ht.Insert(kv);}//删除函数bool erase(const K& key){return _ht.Erase(key);}private:hash_bucket::HashTable<K, pair<K, V>, MapOfK> _ht;};
}
这里修改参数应该难不倒各位同学,接下来我们实现迭代器。
二:实现迭代器
我们已经有了之前的经验,迭代器我们通过一个类来实现,所以这次我们直接声明迭代器类(本质还是指针)。
template<class T>
struct HashTableIterator
{using Node = HashNode<T>;using Self = HashTableIterator; //这里可以不写模板参数//构造函数HashTableIterator(Node* node): _node(node){}Node* _node;
};
三:实现++运算符重载
我们是否可以直接让_node = _node->_next就行了呢?
可以看出,这样不行,当前链表走完了,下一步怎么办?此时我们还没有拿到哈希表最开始位置的指针,这时我们需要将哈希表对象指针传递过来。
template<class T>
struct HashTableIterator
{using Node = HashNode<T>;using Self = HashTableIterator; //这里可以不写模板参数HashTable<K, T, KeyOfT>* _pht;//构造函数HashTableIterator(Node* node): _node(node){}Node* _node;
};
这里是不行的,首先迭代器中的HashTable的这些模板参数根本无法获取,我们必须在HashTableIterator的模板参数中提供。但是此时我们的HashTable在迭代器下方声明定义,编译器器无法识别,所以要在其上面添加其声明。最后我们就算拿到了HashTable的指针,但是我们无法访问其私有成员_table,所以在HashTable中将其声明为友元函数。
并且修改迭代器的构造方法,使其能拿到对应指针;当然因为会重新计算hashi,所以我们需要增加KeyOfT的实例化对象,这里就是kot。
而且我们上面声明了HashTable,在迭代器中使用就要把所有参数都加上,所以我们需要给迭代器再多加一个模板参数——Hash;当然不排除string类型,把Hash也实例化一个对象hs。
//声明HashTable 此时模板参数不需要加缺省值
template<class K, class T, class KeyOfT, class Hash>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct HashTableIterator
{using Node = HashNode<T>;using Self = HashTableIterator; //这里可以不写模板参数KeyOfT kot; //为了计算hashiHash hs;HashTable<K, T, KeyOfT, Hash>* _pht;//构造函数HashTableIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht): _node(node), _pht(pht){}Node* _node;
};
在HashTable中添加声明友元类(先写模板参数,之后友元声明):
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
public:using Node = HashNode<T>;//声明成友元类 先写模板参数template<class K, class T, class KeyOfT, class Hash>friend struct HashTableIterator;//...
};
此时我们就可以完善++了,我们先判断是否还有_next,否则就根据_node重新计算hashi。
Self& operator++()
{if (_node->_next){_node = _node->_next;}else{//重新计算hashisize_t hashi = hs(kot(_node->_data)) % _pht->_table.size();++hashi; //跳过当前桶while (hashi < _pht->_table.size()){if (_pht->_table[hashi]){//此时遇到不为空的节点_node = _pht->_table[hashi];break;}++hashi;}//当然会走到空if (hashi == _pht->_table.size()){_node = nullptr;}}return *this;
}
四:其他运算符的重载
接下来我们实现 != 、== 、* 、-> 这些运算符的重载:
bool operator==(const Self& s)
{return _node == s._node;
}bool operator!=(const Self& s)
{return _node != s._node;
}T& operator*()
{return _node->_data;
}T* operator->()
{return &_node->_data;
}
因为它们太简单了,这里不多做解释。
五:实现begin和end
我们要实现begin和end,先把HashTable的Begin和End实现,Begin就是找第一个不为空的桶;End和之前一样直接拿空构造即可。因为迭代器需要两个参数的构造方法,第二个参数是HashTable的指针,所以我们传入this即可:
//先将其重命名为 Iterator
typedef HashTableIterator<K, T, KeyOfT, Hash> Iterator;//定义Begin
Iterator Begin()
{//找到一个元素for (size_t i = 0; i < _table.size(); ++i){if (_table[i]){return Iterator(_table[i], this);}}//此时没有元素 返回End即可return End();
}//定义End
Iterator End()
{return Iterator(nullptr, this);
}
之后再map和set中实现begin和end,但是此前先对hash_backet的HashTable的Iterator为iterator(有点绕,不过很简单),在set中:
//先重命名
typedef typename hash_bucket::HashTable<K, K, SetOfK>::Iterator iterator;iterator begin()
{return _ht.Begin();
}iterator end()
{return _ht.End();
}
六:实现const类型迭代器
大家知道,按照传统艺能,我们接下来就要将其改变为能够遍历const类型数据的迭代器了,具体细节可以看【C++】通过红黑树封装map和set-CSDN博客 小编的这篇博客,里面对为什么使用以下方法作出了很详细的解释。
还是和之前一样,对迭代器多提供两个参数,一个是引用,一个是指针即可。因为只有 * 和 -> 两个运算符重载返回类型不一样。
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct HashTableIterator
{//...Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Node* _node;
};
所以此时在HashTable中我们这样定义迭代器:
typedef HashTableIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;
typedef HashTableIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;
记得我们之前定义的友元迭代器时要多加上两个参数:
//声明成友元类 先写模板参数
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HashTableIterator;
这里我们就可以实现const版本的Begin和End了。
ConstIterator Begin() const
{for (size_t i = 0; i < _table.size(); ++i){if (_table[i]){return ConstIterator(_table[i], this);}}return End();
}ConstIterator End() const
{return ConstIterator(nullptr, this);
}
之后去完善set中的const版本迭代器:
typedef typename hash_bucket::HashTable<K, K, SetOfK>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, K, SetOfK>::ConstIterator const_iterator;//... 以下为const版本
const_iterator begin() const
{return _ht.Begin();
}const_iterator end() const
{return _ht.End();
}
之后传统艺能,写一个函数来测试,参数是const类型,之后你就发现编译报错了(注意,这里不是一堆报错,只有一个),你双击错误发现停留在HashTable的const版本的End方法中,这是为什么?
这就是我们使用const时,End构造迭代器时,传入的this也是const版本,所以我们在迭代器中需要把_pht指针加上const修饰。记得把构造函数中的 _pht*也加上const修饰。
const HashTable<K, T, KeyOfT, Hash>* _pht;//构造函数
HashTableIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht): _node(node), _pht(pht)
{}
之后再次运行代码就不会有问题了。
七:修改Insert返回值
追剧的同学都知道,Insert返回的值可不是bool,而是一个pair类型。其具体细节可以参考【C++】认识map和set-CSDN博客 这篇文章。
所以这里和之前修改代码是致的:
pair<Iterator, bool> Insert(const T& data)
{//先复用Find//Node* ret = Find(kv.first);Node* ret = Find(kot(data)); //这里要找的是Kif (ret){//存在 无需插入return make_pair(Iterator(ret, this), false);}//...return make_pair(Iterator(newNode, this), true);
}
之后修改set和map中的insert即可。
八:重载map的 [ ]
这里 [ ] 是复用的insert,具体细节可以看【C++】通过红黑树封装map和set-CSDN博客 这篇文章,这里不再赘述,直接上代码:
V& operator[](const K& key)
{iterator it = insert(make_pair(key, V())).first;return it->second;
}
九:修改HashFunc仿函数
如果我们传入的是指针该怎么办?我们应该修改HashFunc这个仿函数,但是我们目前的map和set类中没有这个模板参数,其模板参数在HashTable中,但是我们又不可能跳过map和set给HashTable传入这个仿函数,所以就有问题,我们在写HashTable的时候最后的Hash模板参数不能提供默认的参数,否则有问题,所以我们应该对set和map增加模板参数。
//这里提供第三个默认模板参数
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{//...typedef typename hash_bucket::HashTable<K, pair<K, V>, MapOfK, Hash>::Iterator iterator;typedef typename hash_bucket::HashTable<K, pair<K, V>, MapOfK, Hash>::ConstIterator const_iterator;//...
private:hash_bucket::HashTable<K, pair<K, V>, MapOfK, Hash> _ht;
};
因为我们已经把HashFunc写为全局函数了,所以就这样使用,把HashTable中的缺省模板参数删除即可。
总结:
终于又解决了一个大问题,这里我们C++的水平应该就很强了,之后我们要学习一些有关C++11的新特性,并进入Linux篇,继续学习关于信号等知识,大家敬请期待吧!