当我们需要保存 “键 → 值” 的对应关系,但只关心通过键快速查找或修改值、不需要容器按大小顺序排列时,可以使用 C++ 标准库提供的无序映射 std::unordered_map。
在实际开发中,std::unordered_map 也常被称为 “哈希表” 或 “哈希映射”。它和 std::map 一样要求每个 key 唯一,但底层改用哈希表来组织数据,省去了维护大小排序的开销,平均查找效率更高。
unordered_map 的创建与遍历
使用 std::unordered_map 之前,我们需要先在代码开头引入 <unordered_map> 头文件。
语法:
#include <unordered_map>
std::unordered_map<键类型, 值类型> 变量名 = {
{键1, 值1},
{键2, 值2},
...
};说明:
尖括号 <> 里第 1 个参数是 key 的类型,第 2 个参数是 value 的类型。初始化时每一组键值对同样使用 {key, value} 括起来,不写初始值则会创建一个空的无序映射。
和前面学过的 std::unordered_set 一样,这里的 “无序” 是指容器内部根据 key 计算出的哈希位置来存放数据,既不按 key 的大小排列,也不按插入先后顺序排列。它的常用成员函数如下表所示。
| 操作写法 | 功能说明 |
|---|---|
| um[key] | 访问或修改 key 对应的 value,如果 key 不存在则自动创建 |
| um.at(key) | 访问已存在的 key 对应的 value,如果不存在则抛出异常 |
| um.insert({k, v}) | 插入一组键值对,如果 key 已存在则保留原值 |
| um.erase(key) | 直接按键名删除对应的整组键值对 |
| um.contains(key) | 判断指定的 key 是否存在,返回 true 或 false |
| um.find(key) | 查找 key 所在的位置,找不到则返回 um.end() |
| um.clear() | 清空容器中的所有键值对 |
| um.size() | 返回当前键值对的个数 |
| um.empty() | 判断容器是否为空,为空返回 true |
示例 1:创建并遍历无序映射
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> prices = {
{"Mouse", 99},
{"Keyboard", 299},
{"Monitor", 1299}
};
for (const auto& item : prices) {
std::cout << item.first << ": " << item.second << '\n';
}
return 0;
}运行结果如下。
Monitor: 1299
Keyboard: 299
Mouse: 99分析:
在这个例子中,我们定义了一个名为 prices 的无序映射容器,保存了 3 组商品名称与单价的对应关系。如果把这 3 组商品放进 std::map,它会按商品名的首字母排成 Keyboard、Monitor、Mouse。
而这里的 std::unordered_map 则按底层哈希表的位置输出。在不同编译器或继续添加新元素后,遍历顺序都可能发生变化,因此小伙伴们平时写代码只需要关注每个 key 对应的 value,不要依赖它的遍历顺序。
访问、查找与删除元素
在 C++ 中,std::unordered_map 读写、查找和删除键值对的函数名称与 std::map 完全一致,掌握了 map 就能直接上手。
1. 使用 [] 与 at() 访问和修改
使用 um[key] = value 可以修改已有的键值,或者自动新建不存在的键值。如果只想读取或修改确认已存在的键,可以调用 um.at(key),当传入的键不存在时它会直接抛出异常,不会像 [] 那样意外插入默认元素。
示例 2:修改与新增键值对
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> points = {
{"Jack", 100},
{"Lucy", 150}
};
points["Jack"] = 120; // 修改已有键的值
points["Tony"] = 80; // 自动新增不存在的键
std::cout << "Jack: " << points.at("Jack") << '\n';
std::cout << "Tony: " << points.at("Tony") << '\n';
std::cout << "总人数:" << points.size() << '\n';
return 0;
}运行结果如下。
Jack: 120
Tony: 80
总人数:3分析:
points["Jack"] = 120; 这句代码表示修改已有的 "Jack" 的积分。而 "Tony" 原本不在容器中,因此 points["Tony"] = 80; 会自动新增一组 {"Tony", 80},使总人数变为 3,随后通过 points.at() 安全读取出了对应的积分值。
2. 使用 contains()、find() 与 erase() 查找和删除
查询某个 key 是否存在可以使用 contains(key) 或 find(key),删除某个键值对则调用 erase(key)。
语法:
映射名.contains(key);
auto pos = 映射名.find(key);
映射名.erase(key);说明:
如果我们只是想判断某个 key 在不在容器中,使用 contains(key) 最直观。如果既想判断 key 是否存在,又想紧接着读取或修改它的 value,直接调用 find(key) 并通过迭代器 pos->second 操作值会更高效。
示例 3:查找、修改与删除键值
#include <iostream>
#include <string>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> points = {
{"Jack", 120},
{"Lucy", 150},
{"Tony", 80}
};
auto pos = points.find("Lucy");
if (pos != points.end()) {
pos->second += 30; // 通过迭代器直接修改值
std::cout << pos->first << ": " << pos->second << '\n';
}
points.erase("Jack"); // 按键名删除元素
std::cout << "剩余人数:" << points.size() << '\n';
return 0;
}运行结果如下。
Lucy: 180
剩余人数:2分析:
上面代码中,points.find("Lucy") 会在哈希表中定位 "Lucy" 所在的键值对。确认 pos != points.end() 后,我们直接通过 pos->second += 30; 把她的积分更新为 180。这里同样要注意,左边的键 pos->first 是只读的,只有右边的值 pos->second 可以修改。
相比先调用 contains("Lucy") 判断存不存在、再写 points["Lucy"] += 30 修改的做法,使用 find() 只需要去哈希表里查找一次,避免了重复查表。最后调用 points.erase("Jack") 将 "Jack" 删除,剩余人数变为 2。
使用 unordered_map 统计出现次数
前面我们学过,对映射容器使用 um[key] 时,如果 key 不存在,容器会自动创建这个 key,并把 int 类型的 value 默认初始化为 0。
这个特性在 “统计每个元素出现了多少次” 的场景下非常实用。我们不需要提前写条件判断某个元素是不是第一次出现,直接对 counts[word] 执行自增运算就可以了。请看下面例子。
示例 4:统计单词出现的次数
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
int main() {
std::vector<std::string> words = {
"apple", "banana", "apple", "orange", "apple", "banana"
};
// 保存每个单词出现的次数
std::unordered_map<std::string, int> counts;
for (const std::string& word : words) {
counts[word]++;
}
std::cout << "apple: " << counts["apple"] << '\n';
std::cout << "banana: " << counts["banana"] << '\n';
std::cout << "orange: " << counts["orange"] << '\n';
return 0;
}运行结果如下。
apple: 3
banana: 2
orange: 1分析:
上面代码先使用 std::vector 保存了 6 个水果单词,随后在循环体里只写了一行 counts[word]++;。为什么这一行代码就能自动完成首次创建和后续累加呢?对于 "apple" 来说,它的执行过程等价于下面两步:
// 第一次遇到该键
counts["apple"] = 0;
counts["apple"] = counts["apple"] + 1;
// 后续再次遇到该键
counts["apple"] = counts["apple"] + 1;正是因为 [] 具备 “不存在时自动初始化为 0” 的特点,我们才不需要每次都手写 if 条件去分情况处理,一行 counts[word]++; 就把新建和累加全部搞定了。
哈希容器对键类型的要求
std::unordered_map 之所以能快速定位数据,靠的是标准库提供的 std::hash 根据 key 计算存储位置。C++ 标准库已经为 int、long long、double、std::string 以及指针等常见类型内置了哈希规则,平时直接拿它们当 key 即可。
但像 std::pair、std::vector 或自定义结构体,标准库默认没有提供哈希计算规则,直接拿来作 unordered_map 的 key 会在编译时报错。如果不想手动编写哈希函数,遇到成对坐标作键时直接使用按大小比较的 std::map 会省事得多。
unordered_map 与 map 的区别
std::map 和 std::unordered_map 都用来保存 “键 → 值” 映射关系,在实际开发中我们可以按下面 2 种需求来选择:
- 需要按键的顺序遍历:比如按日期先后汇总每日销量、按学号从小到大打印成绩单,
std::map会一直维护好key的升序状态,遍历出来直接就是排好序的结果。 - 只需按键快速查改:比如根据用户编号查询用户信息、根据配置项名称读取配置值,或者像示例 4 那样统计每个单词出现的次数,不需要排序时优先使用查找速度更快的
std::unordered_map。
