🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
unordered_map和unordered_set
- 🧢unordered_map/set
- 🔮性能比较
- 🔮成员函数
- 🧢改造哈希表
- 🔮增加迭代器
- 类互相typedef时的前置声明
- 友元声明
- 🔮修改哈希表
- 🧢封装哈希表
- 🧢unordered_map的operator[]
- 🧢const迭代器
- 🔮单独定义const迭代器的原因
- 🧢源码
- 🧢总结
🧢unordered_map/set
unordered_map和unorder_set是C++11中才有的,如上图中所示。
- 可以看到,它和我们前面学习的map和set几乎一模一样,只是多了前面的unordered。
正如它的名字一样,unordered_map/set和map/set比起来,unordered_xxx系列的容器打印出来的数据是无序的。
如上图所示,以set和unordered_set为例,乱序插入的数据,set打印出来的数据是升序,而unordered_set打印出来的是无序的。
- set和unordered_set都可以去重。
🔮性能比较
除了这点区别外,unordered_xxx系列容器当然还它自己的优点。
- 生成的随机数是一百万个。
- 比较unordered_set和set对这些数据进行插入,查找,删除花费的时间。
使用三种类型的数据比较:
- 随机数:由于在VS2019下,最多生成的三万多个随机数,所以这一百万个随机数中就会有大量的重复数据,所以在插入到容器中这些数据时会发生降重。
- 几乎没有重复的随机数:在生成随机数的基础上加上1,此时得到是随机数几乎没有重复的,插入到两个容器中。
- 升序数据:插入两个容器中的数据是升序的,比较这种极端情况下两个容器的效率。
无论是哪种类型的数据,无论是插入,查找还是删除,unordered_set的效率都比set高,花费的时间都要少。
- 插入时,升序数据对于set的效率是最高的,因为只需要进行简单的左旋就可以,但是仍然没有unordered_set的效率高。
- 同样,unordered_map的效率也比map的效率要高。
unordered_map/set容器除了不能进行排序外,其他的效率都比map/set效率要高,所以在乱序的场景下,unordered_map/set的使用频率更高。
unordered_map/set的底层是哈希桶。
🔮成员函数
unordered_map/set的使用和map/set的使用一模一样,这么本喵就不重复介绍了,只说明一些不一样的地方。
迭代器只有单向迭代器,没有反向迭代器,因为哈希桶的链表是单链表,所以不支持反向迭代器。
这是一些和挂的桶有关的成员函数,比如获取桶的个数,单个桶的大小等等,但是很少用的上。
这是一些和负载因子等有关的接口,比如获取负载因子,改变负载因子等等,同样很少用的上。
unordered_map/set和map/set不一样的接口主要就是体现在桶的单链表结构上,以及负载因子上。
同样可以用来统计水果个数。
🧢改造哈希表
unordered_map/set的底层使用的是哈希桶,我们需要在本喵的文章哈希表——闭散列 | 开散列(哈希桶)中的哈希桶基础上进行改造。
和map和set的封装一样,为了能让unordered_set和unordered_map使用同一个哈希桶,节点中存放的数据只有一个类型。
将节点中,原本两个模板参数K,V变成一个模板参数T,并且在哈希桶中将节点的类型也做相应的改变。
🔮增加迭代器
迭代器是所有容器必须有的,所以需要给哈希桶增加迭代器以供unordered_set和unordered_map使用。
先来看迭代器的++是如何实现的:
如上图所示,一个哈希表,其中有四个哈希桶,迭代器是it。
++it操作:
- 如果it不是某个桶的最后一个元素,则it指向下一个节点。
- 如果it是桶的最后一个元素,则it指向下一个桶的头节点。
要想实现上面的操作,迭代器中不仅需要一个_node来记录当前节点,还需要一个哈希表的指针,以便找下一个桶,代码如下:
接下来看++it的具体实现:
- it不是处于某个桶的末尾,直接指向下一个节点。
- 当it是某个桶的末尾时,指向下一个桶。
- 首先需要确定当前桶的位置:
使用KeyOfT仿函数获取当前数据的key值(因为不知道是map还是set在调用)。
*再使用Hash仿函数将key值转换成可以模的整形(因为不知道key是整形还是字符串再或者其他自定义类型)。
- 然后开始寻找下一个桶:
从当前哈希表下标开始向后寻找,直到找到下一个桶,将桶的头节点地址赋值给_node。
如果始终没有找到,说明没有桶了,也就是没有数据了,it指向end,这里使用空指针来代替end。
- 将++后的迭代器返回。
上面代码中,获取key值的仿函数方法在map和set的封装中详细讲解过,将key值转换成可以模的整形在哈希表——闭散列 | 开散列(哈希桶)中详细讲解过,这里只是在复用。
这里的两个仿函数在模板参数中都不使用缺省值,因为在封装迭代器的上一层,也就是HashTable中会给它们传参。
类互相typedef时的前置声明
迭代器中有一个成员变量是哈希表的指针,如上图所示,所以在迭代器中typedef了HashTable成为 HT,方便我们使用。
根据我们前面实现迭代器的经验,迭代器其实是封装在Hashtable中的,也就是说,在HashTable中也会typedef迭代器:
- 此时HashTable和HashIterator就构成了相互typedef的关系。
哈希表和迭代器类的定义势必会有一个先后顺序,本喵在定义的时候,在代码顺序上就是先定义迭代器,再定义的哈希表。
此时迭代器在typedef的时候就找不到哈希表的定义,因为编译器只会向上寻找而不会向下寻找。所以必须在HashIterator类前面先声明一下HashTable类,这种操作被叫做前置声明。
- 前置声明一定要放在类外面,如果放在迭代器类里面,编译器只会在迭代器的命名空间中寻找哈希表的定义,这样是找不到的。
- 前置声明放在类外面的时候,编译器会在整个命名空间中寻找哈希表的定义,就可以找到。
友元声明
在++迭代器的时候,会使用到哈希表指针,哈希表指针又会使用到HashTable中的_tables。
- HashTable中的_tables是私有成员,在类外是不能访问的。
解决这个问题可以在HashTable中写一个公有的访问函数,也可以采用友元,本喵这里就是使用的友元的方式。
- 类模板的友元声明需要写模板参数,在类名前面加friend关键字,如上图绿色框中所示。
迭代器中的其他操作,如解引用,箭头,以及相等等运算符的重载本喵就不再详细介绍了,后面本喵会附源码,直接看代码即可。
🔮修改哈希表
- 哈希表的模板参数增加两个仿函数,如上图所示,仍然不使用缺省值,因为会在哈希表的上一层,也就是unordered_set和unordered_set中进行传参。
- 使用typedef封装迭代器,并且给迭代器传对应的模板参数。
还需要在哈希表中增加获取迭代器起始位置和结束位置的接口,如上图代码所示。
- 在获取其实位置时,需要从头开始遍历哈希表项,寻找到第一个桶的头节点作为起始位置。
- 使用空指针代替迭代器的结束位置。
- 在构造迭代器时,直接传this指针去定义迭代器中的哈希表指针。
在插入中,凡是使用到key值以及用key取模的地方,都要用仿函数取获得。包括删除中也是。
🧢封装哈希表
哈希表作为unordered_set和unordered_map的底层结构,是被封装在这两容器中的。
封装比较简单,直接看代码:
unordered_set:
#include "HashTable.h"
namespace wxf
{
template <class K, class Hash = HashFunc<K>>
class unordered_set
{
//获取key值仿函数
struct KeyOfSet
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename HashBucket::HashTable<K, K, Hash, KeyOfSet>::iterator iterator;
//获取begin
iterator begin()
{
return _ht.begin();
}
//获取end
iterator end()
{
return _ht.end();
}
//插入
bool insert(const K& key)
{
return _ht.Insert(key);
}
//查找
bool find(const K& key)
{
return _ht.Find(key);
}
//删除
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
HashBucket::HashTable<K, K, Hash, KeyOfSet> _ht;
};
}
- 获取键值的仿函数在这里实现,并且传给HashTable,转换成可以模的整形的仿函数使用缺失值,具体实现在哈希表的头文件中。
- 在这里也要封装迭代器,要记得加typename,告诉编译器这是一个类型而不是静态变量。
- 迭代器的begin和end包括插入查找删除等操作直接复用HashTable的即可。
- 成员变量是封装的哈希桶。
unordered_map:
#include "HashTable.h"
namespace wxf
{
template <class K, class T, class Hash = HashFunc<K>>
class unordered_map
{
//获取key值仿函数
struct KeyOfMap
{
const K& operator()(const pair<const K, T>& kv)
{
return kv.first;
}
};
public:
typedef typename HashBucket::HashTable<K, pair<const K, T>, Hash, KeyOfMap>::iterator iterator;
//获取begin
iterator begin()
{
return _ht.begin();
}
//获取end
iterator end()
{
return _ht.end();
}
//插入
bool insert(const pair<const K,T>& kv)
{
return _ht.Insert(kv);
}
//查找
bool find(const K& key)
{
return _ht.Find(key);
}
//删除
bool erase(const K& key)
{
return _ht.Erase(key);
}
private:
HashBucket::HashTable<K, pair<const K, T>, Hash, KeyOfMap> _ht;
};
}
代码如上,原理和unordered_set的一模一样,只是这里的数据是键值对,是一个pair类型。
🧢unordered_map的operator[]
再来实现一下unordered_map中的operator[]。
首先,修改哈希表中的Find,让其返回迭代器,如果存在,返回key所在位置的迭代器,如果不存在,返回末尾的迭代器。
然后修改哈希表的Inerst,返回由迭代器和布尔值组成的键值对。
- 先进行查找,如果存在,则返回key所在位置的迭代器和false组成的键值对。
- 查找结构不存在,则返回插入新节点后key所在位置的迭代器和true组成的键值对。
最后修改unordered_map中的insert和find,find返回的是key所在位置的迭代器,复用哈希表的Find,insert返回的是key所在位置的迭代器和bool值组成的键值对,复用哈希表的Insert。
可以看到,使用operator[]成功统计出了水果的个数。
🧢const迭代器
从STL源码中可以看到,const迭代器又重新创建了一个类,并不是和我们之前一样是复用一个类的,这是为什么呢?先来看看和之前一样复用一个类会发生什么?
迭代器中增加const版本的解引用和箭头重载。
在哈希表中定义const迭代器,像之前一样,T&和T*的模板参数都是const类型,再增加两个得到起始和末尾迭代器的接口,如上图代码所示。
在unordere_set中也进行类似的定义,如上图代码所示。
然后就会出现这么长一串错误。
🔮单独定义const迭代器的原因
unordered_set/map的底层是哈希表,所以会用到vector容器,当一个const对象在使用vector的时候,尤其是operator[]接口:
const类型的this指针在调用该接口后,返回的是一个conset类型的引用。在unordered_set/map中返回的就是哈希表中桶节点的const指针。
- 此时桶节点的指针和哈希表的指针都是const类型。
迭代器初始化时,需要的就是桶节点的指针和this指针:
已经存在的是用普通桶节点指针和普通this指针初始化的迭代器,由于现在两个指针都是const类型的,所以我们可以重载构造函数:
HashIterator(const Node* node, const HT* ht)
:_node(node)
,_ht(ht)
{}
但是仍然不行,虽然构造函数的形参和两个const类型的指针一致了,但是,迭代器的成员变量_node和_ht都是普通类型的,用两个const类型的指针初始化两个普通类型的指针,会因为权限放大而报错。
所以需要将迭代器的成员变量_node和_ht也改成const类型的:
此时构造函数就可以正常执行了,但是由于成员变量都变了,const迭代器和普通迭代器势必就不可以使用同一个类了,const迭代器必须单独定义一个类。
- 新建的const迭代器类和普通迭代器类是两个类,所以模板参数中的Ref和Ptr就不需要了,在两个类中单独除了解引用和箭头重载就可以。
普通迭代器中这两个接口是T&和T*,没有const修饰。
同样,和map/set那里一样,需要一个用普通迭代器构造const迭代器的构造函数,因为普通对象的const迭代器使用begin得到的是普通迭代器,需要转换成const迭代器。
普通对象也可以使用const迭代器,此时就需要有普通迭代器向const迭代器转换的构造函数。
const对象必须使用const迭代器。
🧢源码
namespace HashBucket
{
template <class T>
struct HashNode
{
T _data;//存放的数据
HashNode<T>* _next;//下一个节点
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
//哈希表前置声明
template <class K, class T, class Hash, class KeyOfT>
class HashTable;
template <class K, class T, class Hash, class KeyOfT>
struct HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef HashIterator<K, T, Hash, KeyOfT> Self;
Node* _node;//数据节点
HT* _ht;//哈希表指针
HashIterator(Node* node, HT* ht)
:_node(node)
,_ht(ht)
{}
HashIterator()
:_node(nullptr)
, _ht(nullptr)
{}
//解引用重载
T& operator*()
{
return _node->_data;
}
//箭头重载
T* operator->()
{
return &_node->_data;
}
//!=重载
bool operator!=(const Self& it) const
{
return _node != it._node;
}
//==重载
bool operator==(const Self& it) const
{
return _node == it._node;
}
//前置++重载
Self& operator++()
{
//直接指向单链表中的下一个
if (_node->_next)
{
_node = _node->_next;
}
//一个桶已经结束,需要寻找下一个桶
else
{
//确定当前桶在哈希表的位置
KeyOfT kot;
Hash hash;
size_t Hashi = hash(kot(_node->_data)) % _ht->_tables.size();
//寻找下一个桶
++Hashi;
while (Hashi < _ht->_tables.size())
{
//找到下一个桶,it指向头节点
if (_ht->_tables[Hashi])
{
_node = _ht->_tables[Hashi];
break;//++结束
}
++Hashi;
}
//跳出循环有两种情况
//1.所有桶都遍历完了
if (Hashi == _ht->_tables.size())
{
//用空指针充当迭代器的end
_node = nullptr;
}
}
//找到下一个节点,返回迭代器
return *this;
}
//后置++重载
Self operator++(int)
{
Self* ret = this;//记录当前迭代器位置
operator++();
return *ret;
}
};
template <class K, class T, class Hash, class KeyOfT>
struct ConstHashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT;
typedef ConstHashIterator<K, T, Hash, KeyOfT> Self;
typedef HashIterator<K, T, Hash, KeyOfT> iterator;//普通迭代器
const Node* _node;//数据节点
const HT* _ht;//哈希表指针
ConstHashIterator(const Node* node, const HT* ht)
:_node(node)
, _ht(ht)
{}
ConstHashIterator()
:_node(nullptr)
, _ht(nullptr)
{}
ConstHashIterator(const iterator& it)
:_node(it._node)
,_ht(it._ht)
{}
//解引用重载
const T& operator*() const
{
return _node->_data;
}
//箭头重载
const T* operator->() const
{
return &_node->_data;
}
//!=重载
bool operator!=(const Self& it) const
{
return _node != it._node;
}
//==重载
bool operator==(const Self& it) const
{
return _node == it._node;
}
//前置++重载
Self& operator++()
{
//直接指向单链表中的下一个
if (_node->_next)
{
_node = _node->_next;
}
//一个桶已经结束,需要寻找下一个桶
else
{
//确定当前桶在哈希表的位置
KeyOfT kot;
Hash hash;
size_t Hashi = hash(kot(_node->_data)) % _ht->_tables.size();
//寻找下一个桶
++Hashi;
while (Hashi < _ht->_tables.size())
{
//找到下一个桶,it指向头节点
if (_ht->_tables[Hashi])
{
_node = _ht->_tables[Hashi];
break;//++结束
}
++Hashi;
}
//跳出循环有两种情况
//1.所有桶都遍历完了
if (Hashi == _ht->_tables.size())
{
//用空指针充当迭代器的end
_node = nullptr;
}
}
//找到下一个节点,返回迭代器
return *this;
}
//后置++重载
Self operator++(int)
{
Self* ret = this;//记录当前迭代器位置
operator++();
return *ret;
}
};
template <class K, class T, class Hash, class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
//友元声明
template <class K, class T, class Hash, class KeyOfT>
friend struct HashIterator;
template <class K, class T, class Hash, class KeyOfT>
friend struct ConstHashIterator;
public:
typedef HashIterator<K, T, Hash, KeyOfT> iterator;//普通迭代器
typedef ConstHashIterator<K, T, Hash, KeyOfT> const_iterator;//const迭代器
//构造函数
HashTable()
:_n(0)
{
//哈希表10个位置,每个为空
_tables.resize(__stl_next_prime(0), nullptr);
}
//析构函数
~HashTable()
{
for (auto& e : _tables)
{
//释放桶
Node* cur = e;
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
e = nullptr;
}
}
//获取迭代器begin
iterator begin()
{
//寻找第一个桶
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
//使用第一个桶的头节点和哈希表指针构造迭代器
return iterator(_tables[i], this);
}
}
//空哈希表
return iterator(nullptr, this);
}
//获取迭代器end
iterator end()
{
//用空节点代替end
return iterator(nullptr, this);
}
//获取迭代器begin
const_iterator begin() const
{
//寻找第一个桶
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
//使用第一个桶的头节点和哈希表指针构造迭代器
return const_iterator(_tables[i], this);
}
}
//空哈希表
return const_iterator(nullptr, this);
}
//获取迭代器end
const_iterator end() const
{
//用空节点代替end
return const_iterator(nullptr, this);
}
//插入
pair<iterator, bool> Insert(const T& data)
{
KeyOfT kot;
//禁止重复节点插入
iterator ret = Find(kot(data));
if (ret != end())
{
return make_pair(ret, true);
}
//if (Find(kot(data)))
// return false;
//负载因子为1的时候发生扩容
if (_n == _tables.size())
{
新哈希表是原来的二倍
//HashTable newHash;
//newHash._tables.resize(2 * _tables.size(), nullptr);
将旧表中数据插入到表
//for (auto& e : _tables)
//{
// Node* cur = e;
// while (cur)
// {
// newHash.Insert(cur->_kv);
// cur = cur->_next;
// }
//}
现代写法
//_tables.swap(newHash._tables);
//直接创建新的vector,不再创建新的哈希桶
vector<Node*> newTables;
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
for (auto& e : _tables)
{
Node* cur = e;
while (cur)
{
Node* next = cur->_next;//记录下一个节点
//哈希重映射
size_t Hashi = Hash()(kot(cur->_data)) % newTables.size();
//旧表中数据头插到桶中
cur->_next = newTables[Hashi];
newTables[Hashi] = cur;
//迭代cur
cur = next;
}
//将旧表置空,否则会析构掉桶
e = nullptr;
}
//现代写法
_tables.swap(newTables);
}
//哈希映射
size_t Hashi = Hash()(kot(data)) % _tables.size();
//头插
Node* newnode = new Node(data);
newnode->_next = _tables[Hashi];
_tables[Hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
//查找
iterator Find(const K& key)
{
//根据哈希函数直接定位哈希表
size_t Hashi = Hash()(key) % _tables.size();
Node* cur = _tables[Hashi];
//在桶中寻找
while (cur)
{
if (KeyOfT()(cur->_data) == key)
{
//返回key值所在位置
//return cur;
return iterator(cur, this);
}
cur = cur->_next;
}
//return nullptr;
return iterator(nullptr, this);
}
//删除
bool Erase(const K& key)
{
//Node* ret = Find(key);
该值存在,进行删除
//if (ret)
//{
// //删除单链表中的节点
//}
size_t Hashi = Hash()(key) % _tables.size();
Node* cur = _tables[Hashi];
Node* prev = nullptr;
while (cur)
{
//找到
if (KeyOfT()(cur->_data) == key)
{
//key是头节点
if (cur == _tables[Hashi])
{
//直接指向下一个节点
_tables[Hashi] = cur->_next;
}
//key不是头节点
else
{
//key的前一个节点指向key的下一个节点
prev->_next = cur->_next;
}
//删除key所在的节点
delete cur;
--_n;
return true;
}
//继续寻找
else
{
//记录prev
prev = cur;
cur = cur->_next;
}
}
//key不存在,删除失败
return false;
}
//获取素数
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
};
for (int i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return __stl_prime_list[__stl_num_primes - 1];
}
private:
vector<Node*> _tables;//指针数组
size_t _n;
};
};
🧢总结
在unordered_set/map封装哈希表时,首先就是迭代器,要知道迭代器++是怎么移动的,通过计算当前桶的位置去寻找下一个桶,以及反向迭代器需要单独创建一个类的原因和实现。还要知道两个类在互相typedef时,需要有类的前置声明。