本章涵盖无序关联容器,包括:
- 无序关联容器入门
- 如何使用std::unordered_set
- 如何使用std::unordered_multiset
- 如何使用std::unordered_map
- 如何使用std::unordered_multimap
无序关联容器存储一组对象或元素。与前一章学习的(有序)关联容器不同,无序关联容器不使用树状结构和比较函数来排序元素,而是通过哈希函数来组织元素。在许多应用场景中,使用哈希函数替代比较函数能带来更好的性能表现。
无序关联容器入门
在第7章中,你已经了解到关联容器在执行常见操作(如元素插入、搜索和删除)时,其时间复杂度为对数级而非线性级,这与顺序容器形成对比。你还了解到关联容器通常采用红黑树来组织元素,这正是其具备对数级性能特征的根本原因。然而对于许多应用场景而言,这种算法性能的提升仍显不足,特别是在处理包含大量元素的容器时。
为解决这种不平衡问题,某些应用程序会采用无序关联容器。与关联容器不同,无序关联容器不使用内部树结构,也不通过键值比较函数对元素进行排序。相反,无序关联容器利用哈希函数和哈希表来排列其元素。在研究无序关联容器的编程特性之前,有必要先简要了解哈希函数及其他哈希相关概念。
哈希函数与概念
哈希函数是一种将键值映射为固定大小值的函数。这个固定大小的值(通常为整数)被称为哈希值。计算完成后,哈希值会被用作哈希表的索引,该表中存储(或指向)键对应的数据。对于某些容器而言,键与数据是同一个对象。图8-1展示了键、哈希函数、哈希表和数据之间的逻辑关系。

在图8-1中可以看到,哈希表包含N个桶。每个哈希表桶通常是一个键值对(或仅键)的列表。还需注意的是,图中显示许多(但非全部)键会映射到唯一桶。当两个或更多键映射到同一个桶时,就会发生碰撞。这种情况下,桶列表会进行扩展以容纳新元素。
对于任何STL无序关联容器ctr,ctr.bucket_count()返回哈希表桶的数量。哈希表的负载因子是每个桶中元素的平均数量,可按以下公式计算:ctr.load_factor() = ctr.size() / ctr.bucket_count(),其中ctr.size()表示容器元素总数。最大负载因子是允许的负载因子上限值。若因插入元素导致超过该阈值,容器将自动增加桶数量,并执行重哈希操作。重哈希会基于新的桶数量重建整个哈希表,从而减少每个桶维护的元素数量,提升查询性能。但需注意,重哈希操作可能因容器元素规模而耗时较长。
开发者可通过ctr.max_load_factor(float mlf)手动指定无序关联容器的最大负载因子,也可使用ctr.rehash(std::size_t bucket_count_min)直接调整哈希表桶数量。执行ctr.rehash()时,系统会将哈希表桶数量调整为至少bucket_count_min个。需要强调的是,实际桶数量始终由容器自主决定。本章后续将展示这些哈希策略函数的应用实例。
哈希算法的显著优势在于:无需执行键值比较函数或遍历树节点即可确定键在哈希表中的所属桶位置。通过简单索引即可快速访问键对应的哈希表桶,这使得该算法特别适合需要高频查询的应用场景。关于哈希函数与算法的专著浩如烟海,附录B列有相关参考资料供深入研读。
使用std::unordered_set
std::unordered_set是一种无序关联容器,用于存储唯一键的集合。与有序版本std::set类似,std::unordered_set类实现了数学集合的概念。其用于插入、搜索和删除的公共接口与std::set相似。包括std::unordered_set在内的所有STL无序关联容器类,都定义了额外的公共成员函数,以便于定制容器内部的哈希表及哈希策略。
源代码示例Ch08_01展示了std::unordered_set类的几种典型用法。在代码清单8-1-1-1中,请注意头文件Ch08_01.h定义了两个类型别名:uno_set_str_t = std::unordered_set<std::string>和uno_set_hc_t = std::unordered_set<HtmlColor, size_t(*)(const HtmlColor&)>。前者是包含std::string类型键的std::unordered_set,后者则包含HtmlColor类型的键。别名uno_set_hc_t还指定了自定义哈希函数。关于HtmlColor类(如代码清单8-1-4-1所示)和uno_set_hc_t自定义哈希函数的具体细节,将在本节后续部分进行说明。代码清单8-1-1-1还展示了Ch08_01_misc.cpp的源代码,该文件定义了两个print_buckets()函数的重载版本,这些函数用于打印uno_set_str_t或uno_set_hc_t容器的存储桶信息,具体说明见本节后续内容。
//-------------------------------------------------------------------------
// Ch08_01.h
//-------------------------------------------------------------------------
#ifndef CH08_01_H_
#define CH08_01_H_
#include <string>
#include <unordered_set>
#include "Common.h"
#include "HtmlColor.h"
// HtmlColor 值无序集合的类型别名
using uno_set_str_t = std::unordered_set<std::string>;
using uno_set_hc_t = std::unordered_set<HtmlColor, size_t(*)(const
HtmlColor &)>;
// Ch08_01_misc.cpp
void print_buckets(const char *msg, const uno_set_str_t &strings);
void print_buckets(const char *msg, const uno_set_hc_t &colors);
// Ch08_01_ex.cpp
extern void Ch08_01_ex();
#endif
//-------------------------------------------------------------------------
// Ch08_01_misc.cpp
//-------------------------------------------------------------------------
#include "Ch08_01.h"
#include "HtmlColor.h"
template<typename T>
void print_stats(const char *msg, const T &uno_set) {
// 打印stats
std::println("{:s}", msg);
std::print("size: {:d}", uno_set.size());
std::print("bucket_count: {:d}", uno_set.bucket_count());
std::println("load_factor: {:.4f}\n", uno_set.load_factor());
}
void print_buckets(const char *msg, const uno_set_str_t &strings) {
print_stats(msg, strings);
// 打印字符串桶
for (size_t i = 0; i < strings.bucket_count(); ++i) {
if (strings.bucket_size(i) > 0) {
std::print("bucket {:2d}: ", i);
for (auto iter = strings.begin(i); iter != strings.end(i);
++iter)
std::print("'{}' ", *iter);
std::println("");
}
}
}
void print_buckets(const char *msg, const uno_set_hc_t &colors) {
print_stats(msg, colors);
// 打印colors桶
for (size_t i = 0; i < colors.bucket_count(); ++i) {
if (i >= HtmlColor::hash_func_bucket_count)
break;
if (colors.bucket_size(i) > 0) {
std::print("bucket {:2d}: ", i);
for (auto iter = colors.begin(i); iter != colors.end(i);
++iter)
std::print(" {}", iter->Name());
std::println("");
}
}
}
清单8-1-1-2展示了示例函数Ch08_01_ex1()的源代码。该函数的开放代码块实例化了一个uno_set_str_t类型的集合set1,其初始化列表提供了容器的初始值。紧接着的语句调用了print_buckets()函数来打印set1的桶内容。如果提前查看示例Ch08_01的结果部分,会发现set1包含六个元素和八个桶1。其中只有三个桶——编号为三、五、七的桶——包含元素;其余桶为空。同时还显示了set1当前的负载因子,鉴于set1中元素数量较少,这个值目前意义不大。
//-------------------------------------------------------------------------
// Ch08_01_ex.cpp
//-------------------------------------------------------------------------
#include <array>
#include <functional>
#include <string>
#include <unordered_set>
#include "Ch08_01.h"
#include "MF.h"
#include "MT.h"
#include "HtmlColor.h"
uno_set_str_t Ch08_01_ex1() {
// create an unordered set of strings
uno_set_str_t set1{
"Gulf of Alaska", "Caribbean Sea", "Black Sea",
"Red Sea", "Gulf of Thailand", "Bering Sea"
};
print_buckets("\nset1 (initial values): ", set1);
// add more elements using insert and emplace
set1.insert("Baltic Sea");
set1.insert("Mediterranean Sea");
set1.insert("Gulf of Mexico");
set1.emplace("Ross Sea");
set1.emplace("Yellow Sea");
set1.emplace("Greenland Sea");
// add more elements using insert_range (C++23) or ranges::copy (C++20)
std::array<std::string, 6> arr1
{
"Labrador Sea", "Amundsen Gulf", "North Sea",
"Adriatic Sea", "Scotia Sea", "Gulf of Biscay"
};
#if __cpp_lib_containers_ranges >= 202202L
set1.insert_range(arr1);
#else
std::ranges::copy(arr1, std::inserter(set1, set1.begin()));
#endif
print_buckets("\nset1 (after insert_range): ", set1);
return set1;
}
在继续讲解Ch08_01_ex1()之前,我们先快速回顾一下代码清单8-1-1-1中第一个print_buckets()重载的实现。该函数执行时首先调用模板函数print_stats(),该函数会输出std::unordered_set uno_set的基本统计信息,包括uno_set.size()(元素数量)、uno_set.bucket_count()(哈希表桶数量)以及uno_set.load_factor()(每个桶的平均元素数量)。随后通过一个for循环打印每个桶中的元素,其中strings.bucket_size(i)用于获取第i个桶的元素数量。若strings.bucket_size(i) > 0成立,则通过第二个for循环使用迭代器输出该桶的元素——strings.begin(i)和strings.end(i)分别返回第i个桶的起始和结束迭代器。值得注意的是,包括std::unordered_set在内的所有无序关联容器都定义了通用迭代器成员函数begin()、cbegin()、end()和cend()。
回到代码清单8-1-1-2的代码,在首次调用print_stats()后,接下来的代码块演示了std::unordered_set成员函数insert()和emplace()的用法。与其他容器类似,这些函数用于向std::unordered_set插入新元素。随后的代码块通过set1.insert_range(arr1)(C++23特性)或std::ranges::copy(arr1, std::inserter(set1, set1.begin()))将arr1中的std::string元素副本插入set1。其中STL辅助函数std::inserter(set1, set1.begin())构造了一个插入迭代器供std::ranges::copy()使用。
元素插入操作完成后再次调用print_buckets()。请注意结果部分显示set1当前元素数量已增至18个,更重要的是桶数量扩容至64个、负载因子降至0.2812,且单个桶的最大元素数仅为2。正如本节前文所述,当负载因子超过std::unordered_set::max_load_factor()时,无序关联容器会自动增加桶数量并执行重哈希操作。Ch08_01_ex1()的最后一条语句返回set1以供示例函数Ch08_01_ex2()使用(见代码清单8-1-2)。
void Ch08_01_ex2(uno_set_str_t set1) {
// create new set
uno_set_str_t set2{
"Yellow Sea", "Red Sea", "Arabian Sea",
"Baffin Bay",
"North Sea", "Beaufort Sea", "Caspian Sea", "Gulf of Biscay"
};
// set1 is the same as final output from Ch08_01_ex1()
// print_buckets("\nset1 (initial values): ", set1);
print_buckets("\nset2 (initial values): ", set2);
// merge sets
set2.merge(set1);
print_buckets("\nset1 (after merge): ", set1);
print_buckets("\nset2 (after merge): ", set2);
// using contains
std::println("\nset2.contains(\"North Sea\"): {:s}", set2.
contains("North Sea"));
std::println("set2.contains(\"Java Sea\"): {:s}", set2.
contains("Java Sea"));
std::println("");
// using extract ("Green Sea" not member of set2)
std::array<std::string, 3> extract_seas
{"Black Sea", "Green Sea", "Red Sea"};
for (const std::string &extract_sea: extract_seas) {
std::print("extract_sea: {:s}- ", extract_sea);
auto node_handle = set2.extract(extract_sea);
if (!node_handle.empty()) {
std::println("found");
node_handle.value() = MF::to_upper(node_handle.value());
set2.insert(std::move(node_handle));
} else
std::println("not found");
}
print_buckets("\nset2 (after extracts): ", set2);
}
函数Ch08_01_ex2()首先创建了一个名为set2的无序集合。
在调用print_buckets()之后,Ch08_01_ex2()使用set2.merge(set1)将set1中的元素合并到set2中。需要强调的是,std::unordered_set不允许存在重复键值。如结果部分所示,任何同时存在于set2和set1中的键值都将保留在set1内。
完成合并操作后,随后的std::println()调用展示了contains()成员函数的使用场景——当指定的std::string键值存在于set2时,该函数将返回true。
Ch08_01_ex2()最后的代码块演示了std::unordered_set::extract()的用法。这个成员函数会将指定键值(如果存在)从std::unordered_set移动到std::unordered_set::node_type类型的对象中。如后续for循环所示,extract()成员函数常与insert()配合使用来修改键值。
表达式node_handle = set2.extract(extract_sea)会在set2中搜索extract_sea键值。若!node_handle.empty()为真,则说明extract_key已被找到,且该键值已从set2转移到node_handle中。在随后的if代码块内部,
node_handle.value() = MF::to_upper(node_handle.value());
将节点句柄拥有的std::string键值转换为大写后重新保存回该句柄。随后的set2.insert(std::move(node_handle))操作将修改后的键值移回set2集合。此处需要特别说明的是:函数也可以通过erase()/insert()组合来修改std::unordered_set中已存在的键;但当目标容器仅持有可移动对象时,则必须使用extract()/insert()方法。
如代码清单8-1-3所示的Ch08_01_ex3()函数,演示了如何利用迭代器从uno_set_str_t类型的容器中移除以'S'开头的键值。在该函数的for循环内部,表达式(*iter)[0] == 'S'会检查iter所指向的std::string首字母是否等于'S'。若结果为真,后续表达式iter = set1.erase(iter)会从set1中移除iter指向的键值。执行set1.erase(iter)同时会返回指向set1中下一个键值的迭代器。若(*iter)[0] == 'S'判断为假,则通过++iter语句将iter调整为指向set1中的下一个键值。
void Ch08_01_ex3() {
uno_set_str_t set1{
"Superior", "Michigan", "Huron", "Ontario", "Erie",
"Tahoe", "Iliamna", "Crater", "Becharof", "Clark", "Sakakawea",
"Pyramid", "Pontchartrain", "Champlain", "Mead", "Flathead",
"Seneca", "Yellowstone", "Cayuga", "Bear"
};
print_buckets("\nset1 (initial values): ", set1);
// remove strings that begin with an 'S'
for (auto iter = set1.begin(); iter != set1.end();) {
if ((*iter)[0] == 'S')
iter = set1.erase(iter);
else
++iter;
}
print_buckets("\nset1 (after removals): ", set1);
}
本节最后一个源代码示例展示了如何为std::unordered_set指定自定义哈希函数。在继续之前,有必要说明:为特定键类型设计一个算法高效且统计最优的自定义哈希函数并非易事。STL已为所有基本类型定义了哈希函数,除非有充分理由,否则不应弃用这些现成方案。对于用户自定义类,建议结合合适的类属性使用STL预定义的哈希函数。第11章将展示使用预定义哈希函数的示例。
清单8-1-4-1展示了HtmlColor类的源代码。函数Ch08_01_ex4()利用该类来阐释如何创建和使用自定义哈希函数。HtmlColor类在后续示例中也会被使用。
//-------------------------------------------------------------------------
// HtmlColor.h
//-------------------------------------------------------------------------
#ifndef HTML_COLOR_H_
#define HTML_COLOR_H_
#include <cstdint>
#include <format>
#include <functional>
#include <ostream>
#include <string>
#include <vector>
class HtmlColor {
friend struct std::formatter<HtmlColor>;
static const std::vector<HtmlColor> s_HtmlColors; // all HTML colors
public:
// simple structs for RGB and HSI values
struct RG


2454

被折叠的 条评论
为什么被折叠?



