HOWTO · C++
在 C++ 中使用 HashMap
本教程通过解释 C++ 中的 std::map 和 std::unordered_map 来解释如何以正确的方式使用 Hashmap。这也确定了在 C++ 中使用 Hashmap 的关键差异和最佳方式。
本页内容
在 C++ 中,哈希映射由 std::unordered_map 提供;std::map 是相关的键值容器,会按顺序保存键。两者都是关联容器:每个键都唯一,并对应一个映射值。不同的键可以对应相同的值。如果需要按键查找,可以使用其中任意一种;具体选择取决于是否需要按顺序遍历。
C++ 标准没有规定 std::map 必须采用哪种内部实现。它对外呈现的顺序由键的比较规则决定;使用默认比较器时,键按升序排列。相比之下,std::unordered_map 使用哈希函数和桶,其迭代顺序未作规定。不要依赖某次运行中恰好看到的顺序。
C++ 中哈希映射的工作方式
包含 <map> 或 <unordered_map>,并声明键和值的类型,例如 std::map<int, std::string>。insert 仅在键不存在时添加键值对;通过 operator[] 赋值时,如果键尚不存在,容器会先插入一个经过值初始化的映射值,然后再赋值。对于 int 这样的标量映射类型,值初始化会先得到零,再执行赋值。给已有键赋值会更新其值,不会创建重复键。如果要先检查键是否存在再使用其值,可以调用 find。at 也会查找键,但键不存在时会报告 std::out_of_range。
提供的运行记录显示,原始的两个示例都成功运行。下面 std::map 的输出是记录中的实际输出;后面的无序容器示例新增了遍历循环,因此不对修改后示例的输出顺序作任何保证。
使用 std::map 保存有序键
如果需要按比较器确定的顺序遍历键,或需要查找大于等于某个值的首个键等有序操作,请使用 std::map。它的查找、插入和删除操作耗时为对数级,即 O(log n),其中 n 是元素数量。这种可预测的开销和有序遍历适用于生成报告、处理范围以及列出排序后的内容。标准保证的是排序行为,而不是某种特定的树实现。
运行以下示例:
本节中的代码以及显示的标签或输出仍为英文。
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<int, std::string> players;
players.emplace(2, "Lin Dan");
players.emplace(1, "Chen Long");
const auto found = players.find(1);
if (found != players.end()) {
std::cout << found->first << ": " << found->second << '\n';
}
for (const auto& entry : players) {
std::cout << entry.first << ": " << entry.second << '\n';
}
}
1: Chen Long
1: Chen Long
2: Lin Dan
查找操作会打印键 1;循环按比较器顺序遍历键,因此无论插入顺序如何,1 都排在 2 前面。
使用 std::unordered_map 进行基于哈希的查找
如果按键查找是首要需求,而且不需要排序遍历,可以选择 std::unordered_map。查找、插入和删除操作的平均复杂度为 O(1),但最坏情况下可能达到 O(n),例如大量键发生冲突时。重新哈希也可能改变迭代顺序,并使迭代器失效。该容器有自己的哈希和相等性要求:相等的键必须产生相同的哈希值。
运行以下示例:
本节中的代码以及显示的标签或输出仍为英文。
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> scores;
scores["Ada"] = 8;
scores["Lin"] = 11;
const auto found = scores.find("Ada");
if (found != scores.end()) {
std::cout << found->first << ": " << found->second << '\n';
}
for (const auto& entry : scores) {
std::cout << entry.first << ": " << entry.second << '\n';
}
std::cout << "Players: " << scores.size() << '\n';
}
基于范围的循环会遍历 scores 中的每一项。条目可能以任意顺序出现,C++ 标准和此示例都不保证特定的遍历顺序。如果需要按顺序展示,请将键收集到另一个列表中并排序。
如何在 std::map 与 std::unordered_map 之间选择
| 需求 | std::map |
std::unordered_map |
|---|---|---|
| 遍历 | 键按比较器顺序排列 | 不保证迭代顺序 |
| 查找、插入、删除 | O(log n) |
平均 O(1),最坏 O(n) |
| 适用场景 | 需要排序输出、有序范围操作或可预测的对数级操作 | 需要按键查找,但不需要排序遍历 |
| 键的要求 | 比较器提供严格弱序 | 哈希函数与相等比较彼此一致 |
对于词频统计或不关心顺序的缓存,std::unordered_map 通常是自然的选择。需要排序输出或按键进行范围查询时,请使用 std::map。如果无序容器适合查找需求,但展示结果时需要排序,就先收集并排序键,再进行打印,不要假定容器的迭代顺序。选择容器时,除了平均速度,还要考虑所需行为;如果性能对应用很重要,请使用有代表性的数据进行基准测试。
两种容器都支持键值访问,但迭代行为和性能保证不同。遍历时若必须按键排序,请使用 std::map;如果需要基于哈希的查找且不关心顺序,请使用 std::unordered_map。有关有序遍历的另一个示例,请参阅 C++ 中的中序遍历。