摘要
本文深入探讨了 C++
标准库中的两大无序容器——unordered_set
和 unordered_map
,从底层实现、核心操作、性能优化、实际应用等多个方面进行了全面分析。首先,文章介绍了这两种容器的基本概念,说明了它们基于哈希表实现的特点,尤其是在查找、插入和删除操作上具备常数时间复杂度的优势。接着,文章对比了有序容器和无序容器,指出了在不同应用场景下的适用性。
通过对哈希表封装的分析,文章详细讲解了插入、查找和删除操作的底层实现,并阐述了如何通过优化哈希函数、负载因子和重哈希机制来提升容器性能。高阶话题部分讨论了并发哈希表的使用、自定义哈希函数的实现等内容,为更复杂的工程场景提供了技术支持。
此外,本文通过实际案例展示了 unordered_set
和 unordered_map
在邮箱去重、快速键值对查询和 IP
过滤等应用中的具体使用,进一步增强了理论与实践的结合。最后,文章总结了读者通过此博客可以学习到的知识点,帮助读者从基础到高级掌握这两种容器的设计、优化与应用。
本篇博客所涉及的所有代码均可从 优快云 下载区下载
1、引言
在现代 C++
编程中,容器类库提供了各种强大而灵活的数据结构,帮助开发者高效地管理和处理数据。在这些容器中,unordered_set
和 unordered_map
凭借其基于哈希表的实现方式,成为解决查找、去重、数据存储等问题的利器。与传统的有序容器如 set
和 map
不同,unordered_set
和 unordered_map
以其常数时间复杂度 (O(1))
进行查找、插入和删除操作,在处理大量数据时表现得尤为出色。
1.1、unordered_set 和 unordered_map 简介
unordered_set
和 unordered_map
是 C++
标准库中基于哈希表的数据结构。与传统的有序容器 set
和 map
不同,unordered_set
和 unordered_map
并不保持元素的顺序,而是通过哈希函数将元素分配到内存中合适的位置,实现快速查找、插入和删除操作。
-
unordered_set
unordered_set
是一个无序集合容器,旨在存储不重复的元素,并提供快速的插入、查找和删除操作。由于底层使用了哈希表,元素之间没有顺序性,但可以确保每个元素的唯一性。这种容器在需要快速查找和去重时,能够展现出优越的性能,特别是在需要对大量数据进行去重操作时,unordered_set
是首选。 -
unordered_map
unordered_map
则是一个无序键值对容器,类似于字典或映射。它允许存储键-值对,并且同样基于哈希表实现,能够提供常数时间复杂度的查找和插入操作。与unordered_set
类似,unordered_map
的键没有顺序要求,能够快速定位特定的键,并返回其对应的值。其常见的应用场景包括构建缓存、快速查找索引、构建哈希映射等,特别是在需要高效键值查询的场景中,unordered_map
是开发者的理想选择。
1.2、与 set 和 map 的区别
set
和 map
是基于平衡二叉树(如红黑树)实现的有序容器,元素按一定顺序排列,并且支持有序遍历。而 unordered_set
和 unordered_map
并不关注元素的顺序,而是依赖于哈希表来实现常数时间复杂度的操作。他都可以实现 key
和 key/value
的搜索场景,并且功能和使用基本一样。相比于 set
和 map
,这两种无序容器虽然牺牲了数据的顺序性,但通过牺牲排序换来了更高效的查找速度。
在典型的有序容器中,查找、插入和删除操作的时间复杂度为 O(log n)
,这是由于其内部使用了平衡二叉树(如红黑树)实现,降重+排序,遍历结果有序。而 unordered_set
和 unordered_map
使用了哈希表结构,降重不排序,遍历结果无序,理论上可以在常数时间 O(1)
内完成这些操作,极大提高了效率。这使得它们在处理大规模数据集、需要频繁查找或插入的场景中表现得非常优越。
然而,哈希表的高效性并非无代价。由于哈希表的实现依赖于哈希函数,性能的优劣在很大程度上取决于哈希函数的质量。当哈希函数设计不当或发生哈希冲突时,性能可能会下降至 O(n)
级别。此外,unordered_set
和 unordered_map
的内存使用通常高于 set
和 map
,因为哈希表需要额外的空间来处理哈希冲突及动态扩容。
最后,在迭代器方面,map
和 set
是双向迭代器,unordered_map
和 unordered_set
是单向迭代器
如果你对 map
和 set
还不够了解,我的这篇关于 map
和 set
的三万字详解博客请一定不要错过:《 C++ 修炼全景指南:十二 》用红黑树加速你的代码!C++ Set 和 Map 容器从入门到精通
1.3、为什么选择无序容器?
-
时间复杂度的优势
无序容器的最大优势在于其常数时间复杂度O(1)
,这是通过哈希表实现的。相比于有序容器的O(log n)
查找和插入操作,unordered_set
和unordered_map
能够大幅降低处理大量数据时的时间成本。尤其是在元素查找、删除、插入的频率较高时,O(1)
的复杂度意味着这些操作几乎可以在恒定的时间内完成,不会随着数据规模的增加而显著变慢。 -
常数时间复杂度的意义
在典型的应用场景中,如数据库索引、数据查重、实时数据处理等,快速响应的需求非常高。unordered_set
和unordered_map
在查找和插入方面几乎不受数据量的影响,能够保证始终如一的性能表现。这使得它们在需要大规模数据存取、高性能数据处理的应用中成为开发者的首选。此外,哈希表的特性使得冲突的处理(如链地址法、开放地址法)仍能保证较为高效的操作时间,虽然在极端情况下,最坏的情况时间复杂度可能退化为O(n)
,但这通常可以通过设计良好的哈希函数来避免。 -
常见应用场景中的价值
-
缓存(Cache):通过哈希映射实现快速缓存数据访问,能够有效缩短查找时间。
-
快速查找:无序容器由于常数时间复杂度的查找性能,适用于频繁查询的应用场景,如词典查找、用户信息检索等。
-
去重:
unordered_set
在需要去重操作的场景中表现出色,因为它能够快速检测并存储唯一元素。
-
在本文中,我们将深入探讨 unordered_set
和 unordered_map
的技术细节,从其底层实现原理、哈希函数设计,到如何应对哈希冲突和优化性能。我们还会通过代码示例,详细讲解这些容器的常见用法,以及如何在实际项目中合理应用它们。通过对这些内容的深入剖析,本文将帮助读者全面理解这两种无序容器的优势与局限,提供性能分析与优化的建议,并探讨其未来发展方向。
对于 C++
开发者来说,掌握 unordered_set
和 unordered_map
的使用和优化技巧,不仅能够提升代码的性能,还能更好地应对实际开发中的数据管理问题。因此,无论是日常开发还是系统优化,这两种容器都是不可忽视的关键工具。希望通过本文的介绍,读者能够在未来的项目中更加高效、灵活地运用这些容器,编写出更加健壮和高效的 C++
代码。
2、unordered_set 和 unordered_map 的使用与API详解
2.1、unordered_set 使用与 API 详解
unordered_set
是一个集合,它存储唯一值,并且这些值是无序的。它的底层实现是哈希表,因此具有常数时间复杂度 O(1) 的插入、删除和查找操作。
2.1.1、初始化与创建
unordered_set
可以通过多种方式初始化:
#include <unordered_set>
std::unordered_set<int> uset1; // 默认构造一个空的 unordered_set
std::unordered_set<int> uset2 = {1, 2, 3, 4}; // 使用列表初始化
std::unordered_set<int> uset3(uset2.begin(), uset2.end()); // 使用迭代器初始化
std::unordered_set<int> uset4(10); // 初始化时指定 bucket 数量
2.1.2、插入元素
使用 insert()
来向集合插入元素,insert()
返回一个 pair
,其中 first
是指向插入元素的迭代器,second
是一个布尔值,表示是否插入成功(当元素已存在时,插入失败)。
std::unordered_set<int> uset = {1, 2, 3};
auto result = uset.insert(4); // 插入成功, result.second == true
result = uset.insert(3); // 插入失败, result.second == false, 因为 3 已存在
2.1.3、查找元素
通过 find()
来查找元素,返回值是一个指向该元素的迭代器。如果未找到,返回 end()
迭代器。
auto it = uset.find(2); // 找到 2, 返回指向它的迭代器
if (it != uset.end()) {
// 元素存在
} else {
// 元素不存在
}
2.1.4、删除元素
使用 erase()
可以通过值或者迭代器删除元素:
uset.erase(2); // 删除值为 2 的元素
auto it = uset.find(3);
uset.erase(it); // 通过迭代器删除
uset.clear(); // 清空整个集合
2.1.5、查询元素个数
size()
和 empty()
可以用来查询集合的大小和是否为空。
std::cout << uset.size(); // 输出元素个数
std::cout << uset.empty(); // 检查集合是否为空
2.1.6、其他常用 API
count()
:返回元素是否存在(0 或 1)。bucket_count()
:返回哈希表的 bucket 数量。load_factor()
:返回集合的负载因子,用于衡量元素数量和 bucket 的比例。rehash()
:手动调整 bucket 数量以减少哈希冲突。
2.1.7、测试
#include <iostream>
#include <set>
#include <unordered_set>
int main()
{
std::unordered_set<int> us; // Java里面取名叫 HashSet
us.insert(4);
us.insert(2);
us.insert(1);
std::cout << "us 的 bucket 数量是: " << us.bucket_count()
<< "\tus 的 负载因子 是: " << us.load_factor() << std::endl;
us.insert(5);
us.insert(6);
us.insert(3);
us.insert(5);
us.insert(6);
us.insert(3);
us.insert(15);
us.insert(16);
us.insert(13);
std::cout << "us 的 bucket 数量是: " << us.bucket_count()
<< "\tus 的 负载因子 是: " << us.load_factor() << std::endl;
us.insert(15);
us.insert(16);
us.insert(13);
us.insert(9);
us.insert(8);
us.insert(10);
us.insert(7);
us.insert(12);
std::cout << "us 的 bucket 数量是: " << us.bucket_count()
<< "\tus 的 负载因子 是: " << us.load_factor() << std::endl;
// 会去重,但是不会自动排序
std::unordered_set<int>::iterator it = us.begin();
while (it != us.end())
{
std::cout << *it << " ";
++it;
}
std::cout << std::endl;
std::set<int> s; // Java里面取名叫TreeSet
s.insert(4);
s.insert(2);
s.insert(1);
s.insert(5);
s.insert(6);
s.insert(3);
s.insert(5);
s.insert(6);
s.insert(3);
s.insert(15);
s.insert(16);
s.insert(13);
s.insert(15);
s.insert(16);
s.insert(13);
s.insert(9);
s.insert(8);
s.insert(10);
s.insert(7);
s.insert(12);
// set会去重,会自动排序
std::set<int>::iterator its = s.begin();
while (its != s.end())
{
std::cout << *its << " ";
++its;
}
std::cout << std::endl;
return 0;
}
运行结果:

2.2、unordered_map 使用与 API 详解
unordered_map
是一个哈希表结构的键值对容器,每个键 (key)
唯一映射到一个值 (value)
。它也提供了 O(1)
复杂度的插入、删除和查找操作。
unordered_map
是存储<key, value>
键值对的关联式容器,其允许通过 keys 快速的索引到与其对应的value
- 在
unordered_map
中,键值通常用于唯一的标识元素,而映射值是一个对象,其内容与次键关联。键和映射值得类型可能不同- 在内部,
unordered_map
没有对<key, value>
按照任何特定得顺序排序,为了能在常熟范围内找到key
所对应得value
,unordered_map
将相同哈希值得键值对放在相同的桶中unordered_map
容器通过key
访问单个元素要比map
快,但它通常在遍历元素子集的范围迭代方面效率较低unordered_map
实现了直接访问操作(operator[]
),它允许使用key
作为参数直接访问value
2.2.1、初始化与创建
unordered_map
允许使用键值对或迭代器初始化:
#include <unordered_map>
std::unordered_map<int, std::string> umap1; // 默认构造空的 unordered_map
std::unordered_map<int, std::string> umap2 = {
{1, "one"}, {2, "two"}}; // 列表初始化
std::unordered_map<int, std::string> umap3(umap2.begin(), umap2.end()); // 迭代器初始化
std::unordered_map<int, std::string> umap4(10); // 指定 bucket 数量
2.2.2、插入元素
unordered_map
提供多种方式插入键值对:
insert()
:插入键值对。operator[]
:通过键直接插入或更新值。
umap1.insert({3, "three"}); // 使用 insert 插入键值对
umap1[4] = "four"; // 使用 [] 插入或修改值
2.2.3、查找元素
可以使用 find()
或者 operator[]
来查找键对应的值:
auto it = umap1.find(2); // 查找键为 2 的元素
if (it != umap1.end()) {
std::cout << it->second; // 输出值
}
std::cout << umap1[1]; // 如果键不存在,使用 [] 会插入默认值
注意: 使用 operator[]
如果键不存在,会默认插入一个元素,而 find()
则不会。
2.2.4、删除元素
erase()
可以通过键或者迭代器删除键值对:
umap1.erase(2); // 删除键为 2 的元素
auto it = umap1.find(3);
umap1.erase(it); // 通过迭代器删除
umap1.clear(); // 清空整个 map
2.2.5、查询元素个数
和 unordered_set
类似,unordered_map
也提供 size()
和 empty()
来获取大小信息。
std::cout << umap1.size(); // 输出 map 中的键值对个数
std::cout << umap1.empty(); // 检查 map 是否为空
2.2.6、其他常用 API
count()
:判断键是否存在,返回 0 或 1。bucket_count()
和load_factor()
:与unordered_set
一样,用于获取 bucket 信息和负载因子。rehash()
:重新调整 bucket 数量。
2.2.7、测试
#include <iostream>
#include <map>
#include <unordered_map>
int main()
{
std::unordered_map<std::string, std::string> dict;
dict.insert(std::make_pair("hello", "你好"));
dict.insert(std::make_pair("world", "世界"));
dict.insert(std::make_pair("apple", "苹果"));
dict.insert(std::make_pair("orange", "橘子"));
dict.insert(std::make_pair("banana", "香蕉"));
dict.insert(std::make_pair("peach", "桃子"));
dict.insert(std::make_pair("peach", "桃子"));
dict.insert(std::make_pair("peach", "桃子"));
dict.insert(std::make_pair("sort", "排序"));
dict["string"] = "字符串";
std::unordered_map<std::string, std::string>::iterator it = dict.begin();
while (it != dict.end())
{
std::cout << it->first << " " << it->second << std::endl;
++it;
}
std::cout << std::endl;
std::map<std::string, std::string> mdict;
mdict.insert(std::make_pair("hello", "你好"));
mdict.insert(std::make_pair("world", "世界"));
mdict.insert(std::make_pair("apple", "苹果"));
mdict.insert(std::make_pair("orange", "橘子"));
mdict.insert(std::make_pair("banana", "香蕉"));
mdict.insert(std::make_pair("peach", "桃子"));
mdict.insert(std::make_pair("peach", "桃子"));
mdict.insert(std::make_pair("peach", "桃子"));
mdict.insert(std::make_pair("sort", "排序"));
mdict["string"] = "字符串";
std::map<std::string, std::string>::iterator mit = mdict.begin();
while (mit != mdict.end())
{
std::cout << mit->first << " " << mit->second << std::endl;
++mit;
}
return 0;
}
运行结果:

2.3、深入 API 与使用技巧
2.3.1、使用 emplace()
emplace()
是 C++11
引入的一个高效插入元素的方式,它避免了临时对象的创建:
umap1.emplace(5, "five"); // 在 map 中插入键值对
uset.emplace(6); // 在 set 中插入元素
2.3.2、定制哈希函数
如果需要存储用户自定义的类型,可以通过自定义哈希函数来确保性能。unordered_set
和 unordered_map
支持传递自定义哈希器:
struct MyHash
{
std::size_t operator()(const std::pair<int, int>& p) const
{
return std::hash<int>()(p.first) ^ std::hash<int>()(p.second);
}
};
std::unordered_set<std::pair<int, int>, MyHash> customSet;
2.3.3、遍历与迭代
unordered_set
和 unordered_map
都提供迭代器,支持范围 for 循环和迭代器遍历:
for (const auto& elem : uset)
std::cout << elem << " "; // 遍历 unordered_set 中的元素
for (const auto& pair : umap1)
std::cout << pair.first << ": " << pair.second << " "; // 遍历 unordered_map 中的键值对
2.4、小结
unordered_set
和 unordered_map
都是基于哈希表的无序容器,能够在常数时间 O(1) 内进行插入、删除、查找等操作。通过熟练掌握它们的 API 以及性能特征,我们可以在需要高效查找和管理数据的场景中最大化地利用它们的优势。无论是在缓存管理、去重操作还是快速数据查询中,unordered_set
和 unordered_map
都是重要的工具。在实际应用中,选择合适的哈希函数、合理设置负载因子可以有效提高性能并减少哈希冲突,使程序更加高效和稳定。同时,结合自定义哈希函数和高级 API,我们还能够扩展它们的功能,为不同的应用场景提供更优的解决方案。
3、哈希表的基本概念
在C++标准库中,unordered_set
和 unordered_map
都是基于 哈希表(Hash Table) 的数据结构。这种数据结构通过将数据 “哈希化”,即将键通过一个哈希函数映射到一个数组中的某个位置,从而实现常数时间复杂度的查找、插入和删除操作。为了更好地理解 unordered_set
和 unordered_map
的底层工作原理,以下将详细讲解它们的每个关键细节。
3.1、哈希表核心概念
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为 O(N) ,平衡树中为数的高度,即 O(log2N) ,搜索的效率取决于搜索过程中元素的比较次序。
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
哈希表的核心在于将键值对映射到一个数组中的固定位置。这个映射是通过一个 哈希函数 实现的。哈希函数接收一个输入(即键),并根据某种算法生成一个整数,这个整数对应于哈希表的索引位置。
该方式即为哈希 (散列) 方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
- 哈希函数:哈希函数的目标是将键值对尽量均匀地分布到哈希表中,避免 “哈希冲突”(即不同的键映射到同一个数组位置)。
- 哈希桶(Bucket):哈希表的底层结构是一个包含多个 “桶” 的数组,每个桶存放一个或多个键值对。理想情况下,每个键通过哈希函数被映射到唯一的桶中,但实际情况下不可避免地会发生哈希冲突。
3.2、哈希冲突 (Hash Collision)
哈希冲突是指多个不同的键在经过哈希函数计算后,生成了相同的哈希值。当不同的键映射到同一个哈希桶时,称之为哈希冲突。哈希冲突是不可避免的,特别是在哈希表的容量有限的情况下。因此,C++
标准库提供了两种常见的解决哈希冲突的方法:
- 链地址法(Chaining):在每个桶内存储一个链表,当多个键被映射到同一个桶时,将这些键都插入到该桶对应的链表中。插入和查找时,会遍历链表中的元素,找到所需的键值对。
- 开放地址法(Open Addressing):当发生哈希冲突时,使用某种探测方法(如线性探测、二次探测等)寻找下一个空桶存放冲突的键值对。这种方法通过改变元素的存储位置来解决冲突,而不是在每个桶内使用链表。
C++
标准库的 unordered_set
和 unordered_map
采用的是 链地址法,即每个桶存储一个链表。当发生哈希冲突时,冲突的元素会被追加到相应桶的链表中。
由于哈希表依赖于哈希函数将键分散到不同的桶中,避免冲突是优化哈希表性能的关键之一。例如:对于两个数据元素的关键字 ki 和 kj (i != j) ,有 ki != kj ,但有:Hash(ki) == Hash(kj) ,即:不同关键字通过相同哈系计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?其中一个可能的原因:哈希函数
3.3、哈希函数与等价性判断
哈希函数和键的等价性判断是 unordered_set
和 unordered_map
的关键组件。
- 哈希函数(Hash Function):默认情况下,
C++
标准库为基本类型(如整数、浮点数、指针等)提供了标准的哈希函数。例如,std::hash<int>
为整数类型提供哈希支持。如果使用用户定义类型作为键,用户需要提供自己的哈希函数,或者重载std::hash
函数模板。 - 等价性判断(Equality Comparator):即使两个键的哈希值相同,哈希表也需要通过 等价性判断函数 确认它们是否真的是相同的键。
unordered_set
和unordered_map
使用==
操作符判断键是否相等。如果使用自定义类型作为键,用户也需要重载等价性判断操作符。
引起哈希冲突的一个原因是:哈希函数设计的不够合理。哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
哈希函数和等价性判断的工作流程如下:
-
哈希表根据键调用哈希函数,计算出对应的桶索引。
-
如果桶内有多个元素(即发生哈希冲突),则通过等价性判断函数依次检查键是否相等,直到找到匹配的键值对或确定没有该键。
3.4、unordered_set 和 unordered_map 的扩展和负载因子
为了保持哈希表的性能,必须控制哈希表的 负载因子(Load Factor),即哈希表中存储的元素个数与桶的数量之比。当负载因子过高时,哈希冲突会变得频繁,导致查找、插入、删除操作的效率下降。因此,哈希表需要在元素数量达到一定阈值时,进行 扩展(Rehashing)。
- 扩展(Rehashing):当负载因子超过某个预定值(默认情况下,
C++
标准库的负载因子阈值为 1.0,即桶的数量等于元素数量),哈希表会进行扩展。扩展的过程是:- 创建一个新的、更大的桶数组。
- 重新计算所有现有元素的哈希值,并将它们移动到新的桶中。
扩展是一个相对耗时的操作,但它可以确保哈希表的长期性能不受负载因子增加的影响。
3.5、unordered_set 和 unordered_map 的存储结构
虽然 unordered_set
和 unordered_map
都是基于哈希表实现的,但它们存储的内容有所不同:
unordered_set
:存储的是不重复的键。每个键只在哈希表中出现一次,且只进行键的存储和查找操作。unordered_map
:存储的是键-值对(key-value pairs)。每个键关联一个值,可以通过键来查找对应的值。unordered_map
提供的 API 如operator[]
能够轻松通过键访问或修改其关联的值。
3.6、性能优化:哈希函数的选择
unordered_set
和 unordered_map
的性能在很大程度上取决于哈希函数的质量。一个好的哈希函数应该具备以下几个特性:
- 均匀分布:哈希函数应尽量将键均匀分布到各个桶中,避免某些桶过于集中,从而减少哈希冲突。
- 计算效率高:哈希函数的计算应该足够高效,以保证查找、插入和删除操作的整体性能。
- 稳定性:哈希函数的输出应稳定,确保相同的键在不同的操作系统、编译器或运行时环境中生成相同的哈希值。
如果哈希函数设计不当,哈希冲突会频繁发生,导致链表过长,查找性能从 O(1)
退化为 O(n)
。因此,在选择或自定义哈希函数时,需要综合考虑分布性和效率。
常见哈希函数:
1、直接定制法(常用)
取关键字的某个线性函数为散列地址:Hash(key) = A*key + B
。优点:简单,均匀。缺点:需要事先知道关键字的分布情况。使用场景:适合查找比较小且连续的情况。
面试题:字符串中第一个只出现一次的字符
class Solution {
public:
int firstUniqChar(string s) {
// 使用映射的方式统计次数
int count[26] = {0};
for (auto ch : s)
{
count[ch - 'a']++;
}
for (size_t i = 0; i < s.size(); i++)
{
if (count[s[i]-'a'] == 1)
{
return i;
}
}
return -1;
}
};
2、除留余数法(常用)
设散列表中允许地址为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:
Hash(key) = key % p (p <= m)
,将关键码转换成哈希地址
3、平方取中法(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
4、折叠法(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5、随机数法(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即 H(key) = random(key) ,其中 random 为随机数函数。
通常应用于关键字长度不等时采用此法
6、数学分析法(了解)
设有 n 个 d 位数,每一位可能有 r 种不同的符号,这 r 种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
例如:假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是相同的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现冲突,还可以对抽取出来的数字进行反转 (如1234改成4321) 、右环位移(如1234改成4123)、左环移位、前两数与后两数叠加 (如1234改成12+34=46) 等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况
注意:哈系数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
3.7、unordered_set 和 unordered_map 在并发环境下的表现
在多线程环境中,unordered_set
和 unordered_map
并不是线程安全的。这意味着当多个线程同时对同一个容器进行读写操作时,可能会导致数据竞争或不一致。因此,在并发环境下使用时,需要额外的同步机制(如锁)来保证操作的安全性。
不过,现代C++标准库提供了一些支持并发操作的容器(如 concurrent_unordered_map
),可以更高效地在多线程环境下处理无序数据。
4、哈希冲突解决方法及实现
在unordered_set
和unordered_map
中,哈希冲突是一个至关重要的技术问题,直接影响到容器的效率和性能。哈希冲突的详细机制以及如何应对它们,对于理解哈希表的底层实现至关重要。
什么是哈希冲突:
- 哈希冲突是指多个不同的键在经过哈希函数计算后,生成了相同的哈希值。当不同的键映射到同一个哈希桶时,称之为哈希冲突。由于哈希表依赖于哈希函数将键分散到不同的桶中,避免冲突是优化哈希表性能的关键之一。
解决哈希冲突的常见方法:
- 常见的解决哈希冲突的技术主要包括开放地址法(Open Addressing)和链地址法(Chaining),又叫闭散列和开散列。
4.1、开放地址法(Open Addressing)
开放地址法,又叫做闭散列,是解决哈希冲突的主要方法之一,它的核心思想是,当哈希表发生哈希冲突,哈希表中某个位置已经被占用时,如果哈希表未被装满,说明在哈希表种必然还有空位置,通过一系列规则把 key
存放到冲突位置中的 “下一个” 空闲的桶中去。通过一系列规则寻找下一个空闲的桶,而不是使用链表或其他数据结构来存储冲突的元素。与链地址法相比,开放地址法将所有的元素都存储在哈希表的桶内,不使用外部存储,能够节省内存,但对于装载因子的要求比较严格。
那么如何寻找下一个空闲的桶呢?开放地址法的三种常见策略:
- 线性探测(Linear Probing)
- 二次探测(Quadratic Probing)
- 双重哈希(Double Hashing)
4.1.1、基本结构
首先,我们定义了一个哈