`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 后,所有迭代器失效。

删除

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

reserverehash

可能使所有迭代器失效。

不要在遍历时无条件插入大量新键,除非已提前 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()

相关推荐
mengzhi啊1 小时前
QEventLoop loop配合loop.exec();替代while(1)。电脑cpu占用为0,使用while(1)cpu占用率100%
c++·qt
HugoStudio_SWAN1 小时前
洛谷 P1319 / P1320 压缩技术——同一枚硬币的正反面
c++·学习·程序人生·算法
小小晓.2 小时前
C++单例模式万字详解:6种实现演进+线程安全彻底解决+CRTP终极模板
c++·单例模式
j7~3 小时前
【C++11】C++11 新特性全套详解(列表初始化、右值引用、lambda 表达式、function 与 bind 包装器等)
c++·右值引用·lambda表达式·可变参数模板·移动语义·包装器·c++11发展史
无小道3 小时前
C/C++——可变参数
c语言·开发语言·c++·可变参数
hele_two3 小时前
C++模拟蚁群(二)
c++·算法·数学建模·图形渲染
WK100%3 小时前
LeetCode双题:滑动窗口破解最小子数组
c++·经验分享·笔记·算法
@福者4 小时前
C++ QT 入门
开发语言·c++·qt
沧海一笑-dj4 小时前
【C++】Visual Studio 2026下载和安装
c++·visual studio·vs·vs2026
HugoStudio_SWAN4 小时前
洛谷 P1321 单词覆盖还原——从蜡板上的密信到数字水印
c++·学习·程序人生·算法