MSVC版本unordered_set和unordered_map容器

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个构造函数,主要构造这几个部分:

  1. 所存元素,单个元素或者多个元素,可以使用迭代器或者初始化列表
  2. 哈希函数,可以自定义哈希函数
  3. 哈希桶的数目,初始默认为8
  4. key_compare键值比较器,服务于查找等功能
  5. 分配器类型
  6. 其他的就是夹杂着拷贝构造,赋值构造,移动构造等等

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值;不存在则插入节点


如有任何问题,还望指出,谢谢~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值