文章目录
unordered_set容器
以下内容源于MSVC版本的源码分析,花了两天空闲研究源码…
1. unordered_set容器的概述
- MSVC版本的unordered_set的源码不像gcc那么复杂,在继承关系上很简单,unordered_set公有继承于_Hash
template<class _Kty,
class _Hasher = hash<_Kty>,
class _Keyeq = equal_to<_Kty>,
class _Alloc = allocator<_Kty>>
class unordered_set
: public _Hash<_Uset_traits<_Kty,
_Uhash_compare<_Kty, _Hasher, _Keyeq>, _Alloc, false>>
{ // hash table of key values, unique keys
}
- unordered_set只是对_Hash做了简单的封装,提供了28个构造函数和少部分必要的接口,大部分的接口还是被定义在_Hash中,因为是public继承,所以可以被外界调用。
2. unordered_set容器的构造和赋值
unordered_multiset(const unordered_multiset& _Right);
explicit unordered_multiset(size_type _Buckets);
unordered_multiset(size_type _Buckets, const hasher& _Hasharg);
/*
... 等等, 不一一列举
*/
unordered_set提供了28个构造函数,主要构造这几个部分:
- 所存元素,单个元素或者多个元素,可以使用迭代器或者初始化列表
- 哈希函数,可以自定义哈希函数
- 哈希桶的数目,初始默认为8
- key_compare键值比较器,服务于查找等功能
- 分配器类型
- 其他的就是夹杂着拷贝构造,赋值构造,移动构造等等
3. 刨析_Hash底层原理
对于MSVC版本的哈希表,存储过程及原理如下:
3.1 存储结构
_Traits _Traitsobj; // traits to customize behavior
_Mylist _List; // list of elements, must initialize before _Vec
_Myvec _Vec; // vector of list iterators, begin() then end()-1
size_type _Mask; // the key mask
size_type _Maxidx; // current maximum key value
- _Traitsobj:set的特征,包括哈希计算,key_compare等等
- _List:元素的构成的链表,底层就是list
- _Vec:哈希桶,大小为 2 * _Maxidx,所存元素为_List的迭代器;每两个为一组,保存每个桶对应元素的起止迭代器
- _Mask:当前哈希桶的数量 - 1
- _Maxidx:当前哈希桶的数量
3.2 存储过程
存储结构和冲突解决
- 采用vector当作哈希桶,list做元素序列,哈希桶保存list对应元素的迭代器
- 本质采用链地址法
- vector的容量为桶数量的两倍,至于这么做的目的:
- 假设某个元素对应的桶索引为4,则_Vec[2 * 4]保存所有对应4号桶元素链表的起始迭代器,_Vec[2 * 4 + 1]保存终止迭代器 【注意左闭右开区间】
- 说到这里,应该就很明显了,在emplace的过程中,会根据哈希计算的值调整链表元素的相对位置
哈希映射
index = hash & _Mask
- _Mask就是哈希桶的数量 - 1,由于桶的数量都是2的整数次幂,所以-1的结果为
0x0000...11111
,也就是取哈希值的低位 - hash计算的方式
inline size_t _Fnv1a_append_bytes(size_t _Val,
const unsigned char * const _First, const size_t _Count) noexcept
{ // accumulate range [_First, _First + _Count) into partial FNV-1a hash _Val
for (size_t _Idx = 0; _Idx < _Count; ++_Idx)
{
_Val ^= static_cast<size_t>(_First[_Idx]);
_Val *= _FNV_prime;
}
return (_Val);
}
/*
如果在 WIN64环境:
_Val: 14695981039346656037ULL
_FNV_prime: 1099511628211ULL
*/
初始容量与扩容
- 初始的桶的数量为8
- 最大负载因子:1.0;负载因子:容器存储的元素数目 / 桶数量
- 当负载因子超过1.0时,会进行扩容;当桶数目小于512的时候以八倍扩容;否则以两倍扩容
简述一遍过程:
- 存储一个节点时,先使用push_front对list进行头插节点
- 接着调用hash函数获取对应的hash值(用hash表示)
- 使用
index = hash & (buckets - 1)
获取此键值对应存储到的桶编号 - 将_Vec[2 * index] 和 _Vec[2 * index + 1]分别保存所有对应
index
编号桶的元素的链表起止迭代器,同时调整链表顺序,确保对应相同编号桶的元素彼此相邻 - 如果负载因子大于等于1.0,则开始扩容,并且再哈希
rehash
4. unordered_set提供的接口
由于unordered_set是public继承于_Hash,所以基类的接口也被暴露给外界。
关于桶的接口
size_type bucket_count(); // 获取桶的数量
size_type max_bucket_count(); // 获取桶最大数量
size_type bucket(const key_type& _Keyval); // 获取元素对应的桶编号
size_type bucket_size(size_type _Bucket); // 获取编号_Bucket桶的元素个数
local_iterator begin(size_type _Bucket); // 获取编号_Bucket桶元素的起始迭代器
local_iterator end(size_type _Bucket); // 获取编号_Bucket桶元素的终止迭代器
关于插入删除元素
iterator emplace(_Valty&&... _Val);
size_type erase(const key_type& _Keyval);
这个原理也比较简单,不说了,就是list的删除插入操作。
关于list的接口
iterator begin(); // list起始迭代器
iterator end(); // list终止迭代器
size_type size(); // list大小,即所存元素个数
bool empty(); // 判空
关于扩容与hash的接口
float load_factor(); // 获取负载因子
void rehash(size_type _Buckets); // 调整桶数目,再哈希
void reserve(size_type _Maxcount); // 预留桶数目
hasher hash_function(); // 获取 hash函数
关于查找等算法
iterator find(const key_type& _Keyval); // 查找和 _Keyval相匹配的节点, 返回最左端的迭代器
size_type count(const key_type& _Keyval); // 查找和 _Keyval相匹配的节点个数
iterator lower_bound(const key_type& _Keyval);
iterator upper_bound(const key_type& _Keyval);
_Pairii equal_range(const key_type& _Keyval); // 查找和 _Keyval相匹配的节点迭代器范围
由于对应同一个编号桶的元素彼此相邻,且key相等彼此相邻,所以查找的时候是根据桶的起止迭代器进行顺序遍历查找
5. unordered_multiset容器
而对于unordered_multiset容器,和unordered_set几乎一模一样
template<class _Kty,
class _Hasher = hash<_Kty>,
class _Keyeq = equal_to<_Kty>,
class _Alloc = allocator<_Kty>>
class unordered_multiset
: public _Hash<_Uset_traits<_Kty,
_Uhash_compare<_Kty, _Hasher, _Keyeq>, _Alloc, true>> /* 这里改为 true,表示可以重复键值 */
{
}
简单验证上述的_Hash过程
unordered_multiset<int> st;
st.reserve(32);
cout << "*********************" << endl;
list<int> v{1, 34, 67, 0, 20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220, 240, 260, 280,
80, 67, 260, 80};
for (int i : v) {
std::printf("%d的桶编号为:%d\n", i, st.bucket(i));
st.emplace(i);
}
cout << "st 新桶数: " << st.bucket_count() << endl;
cout << "st 新负载因子: " << st.load_factor() << endl;
cout << "st 新最大负载因子: " << st.max_load_factor() << endl;
cout << "*********************" << endl;
for (auto it = st.begin(); it != st.end(); ++it) {
std::cout << *it << " ";
}
/*
result:
1的桶编号为:4
34的桶编号为:23
67的桶编号为:6
0的桶编号为:21
20的桶编号为:1
40的桶编号为:29
60的桶编号为:9
80的桶编号为:5
100的桶编号为:17
120的桶编号为:13
140的桶编号为:25
160的桶编号为:21
180的桶编号为:1
200的桶编号为:29
220的桶编号为:9
240的桶编号为:5
260的桶编号为:6
280的桶编号为:2
80的桶编号为:5
67的桶编号为:6
260的桶编号为:6
180桶编号为:5
st 新桶数:32
st 新负载因子:0.6875
st 新最大负载因子:1.0
1 34 260 260 67 67 160 0 180 20 200 40 220 60 240 80 80 100 120 140 280
*/
6. unordered_map和unordered_multimap容器
unordered_map和unordered_multimap都是在_Hash上封装的,和unordered_set一样,提供了28个构造函数和必要的接口
unordered_map类
template<class _Kty,
class _Ty,
class _Hasher = hash<_Kty>,
class _Keyeq = equal_to<_Kty>,
class _Alloc = allocator<pair<const _Kty, _Ty>>>
class unordered_map
: public _Hash<_Umap_traits<_Kty, _Ty,
_Uhash_compare<_Kty, _Hasher, _Keyeq>, _Alloc, false>> // unordered_multimap这里改为 true
{ // hash table of {key, mapped} values, unique keys
}
鉴于map和set的区别,map保存的节点是pair<key, value>,所以注定会有一些细微的差别:
- at方法
mapped_type& at(const key_type& _Keyval){
// find element matching _Keyval
iterator _Where = _Mybase::lower_bound(_Keyval);
if (_Where == _Mybase::end())
_Xout_of_range("invalid unordered_map<K, T> key");
return (_Where->second);
}
很简单,使用_Hash类定义的查找接口,按照键值查找节点,返回pair的second引用。
- 重载[ ]方法
mapped_type& operator[](const key_type& _Keyval){
// find element matching _Keyval or insert with default mapped
return (try_emplace(_Keyval).first->second);
}
这里有一个try_emplace接口,意义是:如果该节点存在,则返回;不存在则插入节点 fail if _Keyval present, else emplace
- try_emplace方法(上面已介绍)
_Pairib _Try_emplace(_Keyty&& _Keyval,
_Mappedty&&... _Mapval){
// fail if _Keyval present, else emplace
iterator _Where = _Mybase::find(_Keyval);
if (_Where == _Mybase::end())
return (_Mybase::emplace(
piecewise_construct,
_STD forward_as_tuple(_STD forward<_Keyty>(_Keyval)),
_STD forward_as_tuple(_STD forward<_Mappedty>(_Mapval)...)));
else
return (_Pairib(_Where, false));
}
- insert_or_assign接口
_Pairib _Insert_or_assign(_Keyty&& _Keyval,
_Mappedty&& _Mapval){
// assign if _Keyval present, else insert
iterator _Where = _Mybase::find(_Keyval);
if (_Where == _Mybase::end())
return (_Mybase::emplace(
_STD forward<_Keyty>(_Keyval),
_STD forward<_Mappedty>(_Mapval)));
else{
// _Keyval present, assign new value
_Where->second = _STD orward<_Mappedty>(_Mapval);
return (_Pairib(_Where, false));
}
}
注释已经解释的很清楚了,如果节点存在,则修改second值;不存在则插入节点
如有任何问题,还望指出,谢谢~