C++ unordered_map(哈希表)

当我们需要保存 “键 → 值” 的对应关系,但只关心通过键快速查找或修改值、不需要容器按大小顺序排列时,可以使用 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 的大小排列,也不按插入先后顺序排列。它的常用成员函数如下表所示。

std::unordered_map 常用操作
操作写法 功能说明
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。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号