`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()

相关推荐
码匠许师傅8 小时前
【C++ 面试真题】聊聊 C++ 的拷贝构造与拷贝赋值
java·c++·面试
旖旎夜光9 小时前
LeetCode 3:无重复字符的最长子串(滑动窗口) —— 题解
数据结构·c++·算法·leetcode·滑动窗口
汉字萌萌哒9 小时前
2024CSP-J入门级C++真题详解
开发语言·c++
hy.z_77710 小时前
【C++】13. 继承
c++
汉字萌萌哒10 小时前
2019CCF-CSP入门级C++试题解析
java·开发语言·c++
2401_8906034010 小时前
C++ 继承
c++
果果燕11 小时前
C++ 学习笔记(二):类与对象—— 构造函数与析构函数
开发语言·c++
老洋葱Mr_Onion12 小时前
【C++】信息学奥赛CSP通关之路——CSP-J/S第一轮原创全真模拟试卷集(2026)卷四全卷解析
开发语言·c++
布莱克60512 小时前
理解 C/C++ 中的 void* 指针
c语言·c++
小小龙学IT14 小时前
simdjson:利用 SIMD 指令实现 GB/s 级 JSON 解析的 C++ 开源库
开发语言·c++·json