介绍
对于unordered_map来讲,我们使用过或者了解过的人来讲,大致都会知道他的底层数据结构是哈希表。不同于map的底层数据结构是红黑树,本文只针对unordered_map来进行介绍。
如果我们使用过unordered_map,我们也会知道unordered_map是一个key-value形式的结构,并且是一个类模板,我们在使用时通常只传入连个类型,一个是key,一个是value,接下来我们从源码先看一下unorder_map的具体结构。
template<typename _Key, typename _Tp,
typename _Hash = hash<_Key>,
typename _Pred = equal_to<_Key>,
typename _Alloc = allocator<std::pair<const _Key, _Tp>>>
class unordered_map
{
...
}
从上面的结构我们大致可以看出以下几点:
1 该类模板存在五个模板类型,对应的分别是:_Key,_Tp,_Hash,_Pred,_Alloc
2 倒数三个参数都存在默认值,且默认值分别是:hash<_Key>,equal_to<_Key>,allocator<str::pair<const _Key, _Tp>>
各个参数代表的意思
_Key
:key的类型
_Tp
:value值的类型
_Hash
:哈希函数
_Pred
:key相等的比较类模板
_Alloc
:分配器
我们现在大概清楚了以上各个参数的含义,那么我们再来深入的了解一下unordered_map,为什么我们在使用基础数据类型,或者一些其他复合类型时可以直接使用,只需指定两个类型,自定义的类型传入两个却不行。
我们看上图可以知道该类模板提供了三个默认的模板类型,当我们不传递时,会使用这三个默认的模板类型。因此我们在指定类型时确实指定两个就ok了,但是这仅仅针对一些基础类型或者某些复合类型,想要知道原因我们就得扒开他的外衣,看看他的源码实现,因为已经提供了默认了参数,我们深入默认的参数看一下:
对于hash<_Key>
template<> \
struct hash<_Tp> : public __hash_base<size_t, _Tp> \
{ \
size_t \
operator()(_Tp __val) const noexcept \
{ return static_cast<size_t>(__val); } \
};
看上面的代码,我们只是需要了解一下该默认参数的功能作用。我们知道,对于一个哈希的数据结构,那么必然他会有一个特定的哈希函数来计算特定的key的位置,从上面我们可以看出该哈希类模板重载了()运算符,并且返回值类型为size_t。即利用了仿函数的思想,然后计算到了特定的位置。我们暂时不深究他是如何计算key的位置的,我们目前只需要知道hash默认模板参数是通过仿函数,来计算出特定的key的位置。
对于equal_to<_Key>
template<typename _Tp>
struct equal_to : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x == __y; }
};
我们看这个源码,可以看到该源码里传了连个变量,一个是__x,一个是__y,同样是利用了仿函数的思想,并且返回值是bool类型,且直接利用了 __x == __y来比较这两个类型是否相等。
对于最后一个_Alloc默认参数,就是分配资源的,我们使用默认的就好,对我们来讲基本用不到,也无用,因此就不看这个默认参数啦。
unordered_map基础使用
我们看了上面几个参数的源码,不知道大家能否自行分析处我们不需要添加任何东西就能使用这个unordered_map具有什么样的特征?即我们只需要定义unordered_map<keyType, valueType>就可以使用的?
我相信你们肯定是能分析出来的,不过我也大致讲一下,来看看咱们分析的是否一样~
1 key可以是基础数据类型
2 两个key之间可以进行比较(==)
那么满足这两点,就可以直接进行使用,来讲一下原因哈:不能使用的原因就是因为计算机不知道怎么进行处理,如果计算机知道了怎么处理那么自然而然的就可以使用。显然,对于基础数据类型,一定是可以直接进行使用的,因为计算机知道如何处理基础数据类型,所有的复杂类型都是有基础数据类型组合而成的。
那么string类可以当做key来使用吗?我们再重复一下,不能使用的根本原因就是计算机不知道如何处理,只要计算机知道了如何处理,那就可以使用。因为string类内部重载了许多方法,而其中有用的是其重载了 == 这个运算符,因此将两个string类型的key进行直接比较是合理的。对于hash默认模板类,这个我认为在内部其实是实现了针对string的哈希方法,因为是标准库提供给我们使用的类,我没有具体的去看这部分的源码,具体细节的实现不是太了解。因此,要实现一个可用的哈希,那么必然就得使得这两个方法可用,那么我们现在应该就清楚了,我们如果想自定义key方法,那么就需要使得这两个方法可用。知道了这个,那么实现自定义key类型就比较容易了。
unordered_map自定义key类型
设我们的自定义类型如下(两个整形元素组成):
struct myDefine {
myDefine(int a = 1, int b = 2) :m_a(a), m_b(b) {}
int m_a;
int m_b;
};
第一步
对于自定义类型,我们需要实现自己的哈希方法,我们可以仿照标准库给定的默认hash原型来实现,即我们同样实现一个返回值类型为size_t的仿函数。
template<class T>
struct hashh {
size_t operator()(const T& key) const{
return key.m_a + key.m_b;
}
};
如上图,我们实现了一个简单的仿函数哈希,哈希算法就是返回两个属性的和为哈希位置。(这只是随便定义的,你们可以根据自己需求进行定义)
template<typename _Tp>
struct equal_to : public binary_function<_Tp, _Tp, bool>
{
_GLIBCXX14_CONSTEXPR
bool
operator()(const _Tp& __x, const _Tp& __y) const
{ return __x == __y; }
};
第二步
我们观察上面equal_to的这个源码我们就会知道,该仿函数里面就是直接比较这两个key是否相等。因此,我们有两种做法,第一种就是我们使得这两个key能够直接进行相等比较,即我们可以在自己的自定义key里面重载 ==运算符,使得这个key类型可以直接进行相等比较,那么我们就不需要再实现额外的仿函数了。第二种就是我们自己自定义一个仿函数,然后仿照给定的仿函数实现。两种方法如下:
第一种方法
struct myDefine {
myDefine(int a = 1, int b = 2) :m_a(a), m_b(b) {}
bool operator==(myDefine& key2) {
return m_a == key2.m_a;
}
int m_a;
int m_b;
};
第二种方法
template<class T>
struct myEqual {
bool operator()(const T& key1, const T& key2) const {
return key1.m_a == key2.m_a && key1.m_b == key2.m_b;
}
};
经过这两步,我们的自定义key就已经实现完成了,下面我们测试一下
#include <unordered_map>
#include <iostream>
using namespace std;
template<class T>
struct hashh {
size_t operator()(const T& key) const{
return key.m_a + key.m_b;
}
};
struct myDefine {
myDefine(int a = 1, int b = 2) :m_a(a), m_b(b) {}
bool operator==(const myDefine& key2) const{
return m_a == key2.m_a && m_b == key2.m_b;
}
int m_a;
int m_b;
};
template<class T>
struct myEqual {
bool operator()(const T& key1, const T& key2) const {
return key1.m_a == key2.m_a && key1.m_b == key2.m_b;
}
};
int main() {
//自定义key中重载 == 运算符的实现(即第二步的方法一)
unordered_map<myDefine, int, hashh<myDefine>> um1;
um1.insert(make_pair(myDefine(), 6));
um1.insert(make_pair(myDefine(3, 4), 23));
um1.insert(make_pair(myDefine(5, 6), 39));
cout << um1[myDefine()] << endl;
cout << um1[myDefine(3, 4)] << endl;
cout << um1[myDefine(5, 6)] << endl;
cout << "========================================================" << endl;
//自己实现仿函数 即第二步的方法二
unordered_map<myDefine, int, hashh<myDefine>, myEqual<myDefine>> um2;
um2.insert(make_pair(myDefine(), 1));
um2.insert(make_pair(myDefine(3,4), 2));
um2.insert(make_pair(myDefine(5, 6), 3));
cout << um2[myDefine()] << endl;
cout << um2[myDefine(3,4)] << endl;
cout << um2[myDefine(5, 6)] << endl;
return 0;
}
执行结果
注意点:尤其需要注意的就是,自己实现的方法原型一定要和默认提供的方法原型保持一致!!!否则就会报错=====就是传参为const类型,方法最后同样是const类型!!!