`unordered_map` 详解

1. 基本概念

std::unordered_map 是基于哈希表的键值容器。

cpp 复制代码
#include <unordered_map>

特性:

  • 每个键唯一;
  • 键关联一个值;
  • 元素不保证顺序;
  • 平均查找、插入、删除复杂度为 O(1);
  • 最坏情况下可能退化为 O(n)。

2. 基本用法

cpp 复制代码
#include <iostream>
#include <string>
#include <unordered_map>

int main() {
    std::unordered_map<std::string, int> scores;

    scores["Alice"] = 90;
    scores.emplace("Bob", 85);
    scores.insert({"Carol", 95});

    for (const auto& [name, score] : scores) {
        std::cout << name << ": " << score << '\n';
    }
}

遍历顺序不固定。


3. 常用接口

cpp 复制代码
map.empty();
map.size();
map.clear();

map.insert({key, value});
map.emplace(key, value);
map.try_emplace(key, constructor_args...);
map.insert_or_assign(key, value);

map.find(key);
map.contains(key);       // C++20
map.count(key);

map.erase(key);
map.erase(iterator);

map[key];
map.at(key);

4. operator[] 与 at() 的区别

operator[]

cpp 复制代码
std::unordered_map<std::string, int> counts;

int value = counts["unknown"];

若键不存在,会插入:

cpp 复制代码
{"unknown", 0}

因此 operator[] 不适合纯查询场景。

at()

cpp 复制代码
int value = counts.at("camera");

若键不存在,抛出 std::out_of_range。

find()

cpp 复制代码
auto it = counts.find("camera");

if (it != counts.end()) {
    int value = it->second;
}

只查询时通常推荐 find() 或 C++20 contains()。


5. 插入接口区别

5.1 insert

键存在时不覆盖。

cpp 复制代码
auto [it, inserted] = map.insert({"camera", 1});

5.2 emplace

原地构造键值对。

cpp 复制代码
map.emplace("camera", 1);

键已存在时,参数仍可能已经参与临时对象构造。

5.3 try_emplace

键存在时,不构造值对象。

cpp 复制代码
map.try_emplace("camera", expensive_constructor_arg);

当值对象构造昂贵时更合适。

5.4 insert_or_assign

键不存在则插入,存在则覆盖值。

cpp 复制代码
map.insert_or_assign("camera", 2);

5.5 operator[]

适合计数和确保键存在。

cpp 复制代码
++word_count[word];

6. 典型计数场景

cpp 复制代码
#include <string>
#include <unordered_map>
#include <vector>

std::unordered_map<std::string, int> countWords(
    const std::vector<std::string>& words
) {
    std::unordered_map<std::string, int> counts;
    counts.reserve(words.size());

    for (const auto& word : words) {
        ++counts[word];
    }

    return counts;
}

7. 哈希表基础

unordered_map 通常包含:

  • 桶数组;
  • 根据哈希值分配到桶中的节点;
  • 桶内冲突处理结构。

查找过程通常是:

  1. 计算键的哈希值;
  2. 根据桶数量定位桶;
  3. 在桶内使用相等比较器寻找目标键。

8. 装载因子与扩容

cpp 复制代码
std::unordered_map<int, std::string> devices;

devices.reserve(1000);
devices.max_load_factor(0.75f);

常用状态:

cpp 复制代码
devices.bucket_count();
devices.load_factor();
devices.max_load_factor();

装载因子定义:

text 复制代码
load_factor = size / bucket_count

当元素数量增大到一定程度,容器会 rehash:

  • 增加桶数量;
  • 重新分配所有元素到新桶;
  • 使迭代器失效;
  • 产生一次较明显的性能开销。

提前调用 reserve() 可以减少扩容次数。


9. 自定义键类型

cpp 复制代码
#include <functional>
#include <string>
#include <unordered_map>

struct SensorKey {
    int device_id;
    int channel;

    bool operator==(const SensorKey& other) const {
        return device_id == other.device_id &&
               channel == other.channel;
    }
};

struct SensorKeyHash {
    std::size_t operator()(const SensorKey& key) const {
        std::size_t h1 = std::hash<int>{}(key.device_id);
        std::size_t h2 = std::hash<int>{}(key.channel);

        return h1 ^ (h2 << 1);
    }
};

std::unordered_map<SensorKey, std::string, SensorKeyHash> sensors;

必须满足:

text 复制代码
若 key1 == key2,则 hash(key1) == hash(key2)

10. 组合哈希函数

简单组合:

cpp 复制代码
std::size_t combineHash(std::size_t seed, std::size_t value) {
    seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
    return seed;
}

使用:

cpp 复制代码
struct PointHash {
    std::size_t operator()(const Point& p) const {
        std::size_t seed = 0;
        seed = combineHash(seed, std::hash<int>{}(p.x));
        seed = combineHash(seed, std::hash<int>{}(p.y));
        return seed;
    }
};

哈希组合需要兼顾:

  • 分布均匀;
  • 计算速度;
  • 与相等规则一致;
  • 对输入模式不敏感。

11. 值类型为复杂对象

cpp 复制代码
struct DeviceState {
    int status;
    std::string message;
};

std::unordered_map<int, DeviceState> states;

states.try_emplace(
    1001,
    DeviceState{1, "online"}
);

通过结构化绑定遍历:

cpp 复制代码
for (auto& [id, state] : states) {
    state.status = 0;
}

只读遍历:

cpp 复制代码
for (const auto& [id, state] : states) {
}

12. 值类型为智能指针

cpp 复制代码
#include <memory>

std::unordered_map<int, std::unique_ptr<Device>> devices;

devices.emplace(
    1,
    std::make_unique<Device>()
);

这可以清晰表达:

  • 容器拥有对象;
  • 元素删除时对象自动析构;
  • 避免手动 delete。

13. 迭代器失效规则

插入

  • 未触发 rehash 时,已有迭代器通常保持有效;
  • 触发 rehash 后,所有迭代器失效。

删除

  • 指向被删除元素的迭代器、指针、引用失效;
  • 其他元素通常不受影响。

reserve 和 rehash

可能使所有迭代器失效。

不要在遍历时无条件插入大量新键,除非已提前 reserve() 或正确处理迭代器失效。


14. 遍历中安全删除

cpp 复制代码
for (auto it = map.begin(); it != map.end(); ) {
    if (shouldErase(it->first, it->second)) {
        it = map.erase(it);
    } else {
        ++it;
    }
}

C++20:

cpp 复制代码
std::erase_if(map, [](const auto& item) {
    return item.second.expired();
});

15. 与 map 的区别

对比项 unordered_map map
底层结构 哈希表 平衡搜索树
元素顺序 无序 按键有序
平均查找 O(1) O(log n)
最坏查找 O(n) O(log n)
范围查询 不支持 支持
哈希函数 需要 不需要
比较器 相等比较 顺序比较
迭代顺序 不稳定 稳定有序
内存特点 桶数组和节点 树节点指针

16. 常见应用

  • 字符串词频统计;
  • ID 到对象映射;
  • 缓存;
  • 配置项查询;
  • 状态表;
  • 图的邻接表;
  • 去重并附加属性;
  • 消息类型到处理函数映射;
  • 稀疏数据索引。

处理函数映射:

cpp 复制代码
#include <functional>
#include <unordered_map>

using Handler = std::function<void()>;

std::unordered_map<int, Handler> handlers;

handlers.emplace(1, [] {
    // process type 1
});

17. 性能优化建议

  1. 已知规模时调用 reserve()。
  2. 设计分布良好的哈希函数。
  3. 避免在热点路径反复构造临时字符串键。
  4. 对字符串可考虑异构查找。
  5. 复杂值类型优先使用 try_emplace()。
  6. 更新已有值可使用 insert_or_assign()。
  7. 不依赖遍历顺序。
  8. 对强实时、极端最坏复杂度敏感的场景,评估 map 或专用哈希容器。
  9. 小数据集先进行基准测试,vector 线性查找可能更快。
  10. 不要通过过低的 max_load_factor 盲目换取速度,以免内存显著增加。

18. 常见错误

18.1 查询时意外插入

cpp 复制代码
if (map[key] == 1) {
}

若键不存在,会创建新元素。

推荐:

cpp 复制代码
auto it = map.find(key);

if (it != map.end() && it->second == 1) {
}

18.2 修改键

键在容器中视为 const,不能直接修改,否则会破坏哈希结构。

需要修改键时:

  • 删除旧键;
  • 插入新键;
  • 或使用 C++17 节点句柄。
cpp 复制代码
auto node = map.extract(old_key);

if (!node.empty()) {
    node.key() = new_key;
    map.insert(std::move(node));
}

18.3 假设哈希冲突不会发生

哈希值相同不代表键相等。容器仍需调用相等比较器。

18.4 暴露不稳定顺序

序列化、测试和日志如果要求确定顺序,应先排序键或使用 map。


19. 面试总结

unordered_map 是哈希键值容器,平均查找、插入和删除复杂度为 O(1),但不保证遍历顺序,最坏情况可能退化为 O(n)。其性能受哈希函数、桶数量和装载因子影响。

使用时应注意 operator[] 会在键不存在时插入默认值,提前知道规模时可调用 reserve(),复杂值构造建议使用 try_emplace()。

相关推荐
沫璃染墨3 小时前
从零入门计算机网络系列(十二):HTTP长连接与会话管理详解——从Connection到Cookie与Session
linux·服务器·c++·tcp/ip·ubuntu·http·交互
xiaoye-duck4 小时前
《C++ 项目:手写大模型接入 SDK》DeepSeek接口设计(下):流式响应实现和测试
开发语言·网络·c++·语言模型
旖旎夜光6 小时前
【C++ 基础入门】类和对象(上):封装、对象模型与 this 指针
开发语言·c++·笔记·学习
小超人诞生日记6 小时前
C++ 智能指针的使用及其原理详解
c++·算法
akinggw7 小时前
libagentcore - 一个基于c++的AI多agent系统升级到2.0版本
开发语言·jvm·c++
繁星蓝雨8 小时前
C++设计原理———多重继承(虚基类、菱形继承、委托、重命名、基类和成员初始化)
c++·委托·多重继承·mi·显式初始化列·虚基类·对象布局
旖旎夜光9 小时前
【C++ 基础入门】命名空间、输入输出、缺省参数与函数重载
开发语言·c++·笔记·学习
扶尔魔ocy10 小时前
【QT入门到晋级】用开源的自定义控件开发
c++·qt·自定义控件
akinggw11 小时前
libsherpacore - 基于 sherpa-onnx 的 C++ 语音交互静态库
c++·交互·语音识别
xiaoye-duck11 小时前
《C++ 项目:手写大模型接入 SDK》ChatGPT接口设计(中):全量响应实现和测试
网络·c++·语言模型·chatgpt