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

相关推荐
一拳一个呆瓜3 小时前
【STL】iostream 编程:检测提取操作产生的错误
c++·stl
稚南城才子,乌衣巷风流3 小时前
判断素数与拓展应用
c++
众少成多积小致巨3 小时前
C++ 规范参考(下)
c++
c238563 小时前
把 C++ 内存分配拆透:new 与 malloc 的三层血缘
开发语言·c++·算法
txzrxz4 小时前
最短路问题——Dijkstra 算法
数据结构·c++·算法·最短路·优先队列
noipp5 小时前
推荐题目:洛谷 P6231 [JSOI2013] 公交系统
c语言·数据结构·c++·算法·游戏·洛谷·luogu
c238565 小时前
C/C++每日一练6
c语言·c++·算法
念恒123065 小时前
网络基础
linux·网络·c++
mct1236 小时前
c++ iconv 字符utf-8转换gb2312失败
开发语言·c++