- 和关联式容器一样,无序容器也使用键值对(pair 类型)的方式存储数据。不过,本教程将二者分开进行讲解,因为它们有本质上的不同:
关联式容器的底层实现采用的树存储结构,更确切的说是红黑树结构;
无序容器的底层实现采用的是哈希表的存储结构。 - C++ STL 底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)
- 基于底层实现采用了不同的数据结构,因此和关联式容器相比,无序容器具有以下 2 个特点:
a. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,
和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1))
b. 但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。
unordered_map容器
模版
template
< class Key,
class T,
class Hash = hash<Key>, //容器内部存储键值对所用的哈希函数
class Alloc = allocator<pair<const Key, T>> //判断各个键值对键相同的规则,默认情况下,使用 STL 标准库中提供的 equal_to<key> 规则,该规则仅支持可直接用 == 运算符做比较的数据类型。
>
std::unordered_map<std::string, std::string> umap;
umap.emplace("Python教程","http://c.biancheng.net/python/");
umap.emplace("Java教程", "http://c.biancheng.net/java/");
umap.emplace("Linux教程", "http://c.biancheng.net/linux/");
//遍历
for(auto it = umap.begin(); it != umap
.end(); ++it) {
std::cout << it->first << " " << it->second << std::endl;
}
/*
Linux教程 http://c.biancheng.net/linux/
Java教程 http://c.biancheng.net/java/
Python教程 http://c.biancheng.net/python/
*/
这些内容都比较无聊,看点别的
STL无序容器底层实现原理(深度剖析)
- C++ STL 标准库中,不仅是 unordered_map 容器,所有无序容器的底层实现都采用的是哈希表存储结构。更准确地说,是用“链地址法”(又称“开链法”)解决数据存储位置发生冲突的哈希表,整个存储结构如图 :
- 其中,Pi 表示存储的各个键值对。
- 可以看到,当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。
- TL 标准库通常选用 vector 容器存储各个链表的头指针。有意思~
- 不仅如此,在 C++ STL 标准库中,将图 1 中的各个链表称为桶(bucket),每个桶都有自己的编号(从 0 开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:
- 将该键值对中键的值带入设计好的哈希函数,会得到一个哈希值(一个整数,用 H 表示);
- 将 H 和无序容器拥有桶的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;
- 建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上。
负载因子(load factor)
负载因子 = 容器存储的总键值对 / 桶数
- 默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。
- 需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。
- 这也就解释了,为什么我们在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。
成员方法 功能
bucket_count() 返回当前容器底层存储键值对时,使用桶的数量。
max_bucket_count() 返回当前系统中,unordered_map 容器底层最多可以使用多少个桶。
bucket_size(n) 返回第 n 个桶中存储键值对的数量。
bucket(key) 返回以 key 为键的键值对所在桶的编号。
load_factor() 返回 unordered_map 容器中当前的负载因子。
max_load_factor() 返回或者设置当前 unordered_map 容器的最大负载因子。
rehash(n) 尝试重新调整桶的数量为等于或大于 n 的值。如果 n 大于当前容器使用的桶数,则该方法会是容器重新哈希,该容器新的桶数将等于或大于 n。反之,如果 n 的值小于当前容器使用的桶数,则调用此方法可能没有任何作用。
reserve(n) 将容器使用的桶数(bucket_count() 方法的返回值)设置为最适合存储 n 个元素的桶数。
hash_function() 返回当前容器使用的哈希函数对象。
//创建空的umap1
std::unordered_map<std::string, std::string> umap1;
std::cout << "umap初始捅数 :" << umap1.bucket_count()<<std::endl;
std::cout << "umap初始负载因子 :" << umap1.load_factor() << std::endl;
std::cout << "umap 最大负载因子 : " << umap1.max_load_factor() << std::endl;
/*
umap初始捅数 :0
umap初始负载因子 :0
umap 最大负载因子 : 1
*/
//设置 umap 使用最适合存储 9 个键值对的桶数
umap1.reserve(9);
std::cout << "umap新捅数 :" << umap1.bucket_count()<<std::endl;
std::cout << "umap新负载因子 :" << umap1.load_factor() << std::endl;
/*
umap新捅数 :11
umap新负载因子 :0
*/
umap1["Python教程"] = "http://c.biancheng.net/python/";
umap1["Java教程"] = "http://c.biancheng.net/java/";
umap1["Linux教程"] = "http://c.biancheng.net/linux/";
std::cout << "umap新捅数 :" << umap1.bucket_count()<<std::endl;
std::cout << "umap新负载因子 :" << umap1.load_factor() << std::endl;
//调用 bucket() 获取指定键值对位于桶的编号
std::cout << "以\"Python教程\"为键的键值对,位于桶的编号为:" << umap1.bucket("Python教程") << std::endl;
//自行计算某键值对位于哪个桶
auto fn = umap1.hash_function();
std::cout << "计算以\"Python教程\"为键的键值对,位于桶的编号为:" << fn("Python教程") % (umap1.bucket_count()) << std::endl;
关于哈希表
- 常用的哈希函数的构造方法有 6 种:直接定址法、数字分析法、平方取中法、折叠法、除留余数法和随机数法。
- 直接定址法
H(key)= key 或者 H(key)=a * key + b
-
数字分析法: 果关键字由多位字符或者数字组成,就可以考虑抽取其中的 2 位或者多位作为该关键字对应的哈希地址,在取法上尽量选择变化较多的位,避免冲突发生。
-
平方取中法是对关键字做平方操作,取中间得几位作为哈希地址。此方法也是比较常用的构造哈希函数的方法。
例如关键字序列为{421,423,436},对各个关键字进行平方后的结果为{177241,178929,190096},则可以取中间的两位{72,89,00}作为其哈希地址。
- 折叠法 是将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。此方法适合关键字位数较多的情况。
- 除留余数法:若已知整个哈希表的最大长度 m,可以取一个不大于 m 的数 p,然后对该关键字 key 做取余运算,即:
H(key)= key % p。
处理冲突的方法
- 开放定址法
H(key)=(H(key)+ d)MOD m(其中 m 为哈希表的表长,d 为一个增量)
- 当得出的哈希地址产生冲突时,选取以下 3 种方法中的一种获取 d 的值,然后继续计算,直到计算出的哈希地址不在冲突为止,这 3 种方法为:
- 线性探测法:d=1,2,3,…,m-1
- 二次探测法:d=12,-12,22,-22,32,…
- 伪随机数探测法:d=伪随机数
-
再哈希法
当通过哈希函数求得的哈希地址同其他关键字产生冲突时,使用另一个哈希函数计算,直到冲突不再发生。 -
链地址法
将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中。例如有一组关键字为{19,14,23,01,68,20,84,27,55,11,10,79},其哈希函数为:H(key)=key MOD 13,使用链地址法所构建的哈希表如图 3 所示: