C++ std::map 与 std::unordered_map

前言

std::map 和 std::unordered_map 是 C++ STL 中最常用的两种关联容器,它们都用于存储 key-value 键值对,且 key 唯一。两者功能非常相似,但底层实现、性能特性、适用场景完全不同。接下来我将详细讲解两者的区别,以及分别在什么场景下使用:

一、两者的区别

先看结论,了解 std::map 与std::unordered_map 的核心区别:

对比维度 std::map std::unordered_map
底层数据结构 红黑树 哈希表
元素有序性 有序(按key升序排列) 无序(遍历顺序不固定)
查找/插入/删除时间复杂度 O(log n) 平均 O(1),最坏 O(n)
内存占用 较高(每个节点需 3 个指针 + 颜色) 一般较高(桶 + 负载因子)
key 要求 需要 < 比较(或自定义comparator) 需要 == 比较 + std::hash
适用数据量 中小数据量(几千~几万) 大数据量(几十万以上)
典型使用场景 需要有序、范围查询、稳定性能 追求极致速度、不关心顺序

二、底层实现原理

1.std::map ------ 红黑树

属于有序关联容器,内部维护一棵自平衡的二叉查找树。插入、删除、查找时自动调整树形,保证树的高度始终为 O(log n),因此所有操作都有严格的 O(log n) 上界,性能非常稳定。

2.std::unordered_map ------ 哈希表

属于无序关联容器,内部是一个桶数组 + 链表/链地址法(C++11 后)。通过 std::hash 计算 key 的哈希值,决定存放在哪个桶。平均情况下 每个桶的元素数量很少(查找几乎是 O(1)),但当哈希冲突严重或负载因子过高时,会发生 rehash(重新分配桶),最坏情况退化为 O(n)。

三、差异详细讲解

1. 有序性与遍历顺序

std::map:迭代器遍历时严格按 key 排序(默认 std::less)。

std::unordered_map:迭代器遍历顺序与插入顺序无关,取决于哈希值和桶的分布。即使同一个程序多次运行,遍历顺序也可能不同。

2. 时间复杂度与性能

查找(find、count、operator[]):

map:O(log n);

unordered_map:平均 O(1),最坏 O(n)。

插入/删除:同上。

范围查询(如查找 [low, high] 区间所有元素):

map:lower_bound + upper_bound 非常高效;

unordered_map:无法高效实现,只能线性扫描。

3. 内存与缓存友好性

map:节点分散在堆上,缓存不友好(指针跳跃)。

unordered_map:桶数组连续,缓存相对友好,但负载因子(默认 1.0)会导致额外内存浪费。

4. key 类型要求

std::map:

cpp 复制代码
// 只需实现 operator< 即可(或提供 Comparator)
bool operator<(const MyKey& other) const;

std::unordered_map:

cpp 复制代码
// 必须同时提供 hash 和 ==(或 equal_to)
struct MyHash { size_t operator()(const MyKey& k) const; };
struct MyEqual { bool operator()(const MyKey& a, const MyKey& b) const; };

内置类型(int、string等)已提供。

5. 迭代器失效

map:仅被删除的迭代器失效,其他迭代器仍有效。

unordered_map:rehash 时所有迭代器都可能失效(insert、rehash、clear 等都可能触发)。

6. 独有成员函数

std::map 特有:

lower_bound、upper_bound、equal_range、key_comp()、value_comp()

std::unordered_map 特有:

bucket_count()、load_factor()、max_load_factor()、rehash()、reserve()

bash 复制代码
------------std::map  独有成员函数---------------
 
lower_bound(key) //返回第一个不小于  key  的元素迭代器,用于有序范围查找。

upper_bound(key) //返回第一个大于  key  的元素迭代器,配合  lower_bound  可获取等于  key  的元素范围。

equal_range(key) //返回一对迭代器,分别指向等于  key  的元素范围的首尾(等价于  {lower_bound, upper_bound} )。

key_comp() //返回用于比较键的比较器对象,用于自定义键的排序逻辑。

value_comp() //返回用于比较键值对的比较器对象,本质是基于  key_comp()  封装。
 
-------------std::unordered_map  独有成员函数-------------
 
bucket_count() //返回当前哈希桶的总数。

load_factor() //返回当前负载因子(元素总数 / 桶数),用于衡量哈希表的拥挤程度。

max_load_factor() //获取或设置允许的最大负载因子,超过则自动触发  rehash 。

rehash(n) //重新哈希,将桶数调整为至少  n ,会重新分配所有元素。

reserve(n) //预分配空间,使容器能容纳至少  n  个元素而不触发  rehash ,优化插入性能。

四、std::map 与std::unordered_map的使用场景

推荐使用 std::map 的场景

  1. 需要维护有序性

    日志按时间戳排序输出;

    字典/配置按 key 字母顺序展示;

    需要按 key 范围查询(如查找所有 ID 在 [1000, 2000] 的记录)。

  2. 对最坏情况性能敏感

    实时系统、游戏引擎、服务器核心路径;

    不能容忍哈希碰撞导致的 O(n) 卡顿。

  3. 数据量不大(< 10万量级)

    log n 在现代 CPU 上极快(log₂(100000) ≈ 17)。

  4. key 难以设计好的 hash 函数

    自定义复杂结构体、对象指针等。

  5. 需要频繁做范围操作

    区间统计、滑动窗口类问题。

推荐使用 std::unordered_map 的场景

  1. 追求极致速度

    大规模数据去重、计数(LeetCode 频率统计题);

    缓存(LRU Cache 通常配合 list + unordered_map)。

  2. 数据量巨大(10万 ~ 千万级)

    此时 O(1) 的优势非常明显。

  3. 不关心顺序

    仅需快速 insert / find / erase。

  4. key 是内置类型或容易哈希

    int、string、uint64_t、pair<int,int> 等。

  5. 内存不是瓶颈

    愿意用空间换时间。

代码示例:

cpp 复制代码
#include <iostream>
#include <map>
#include <unordered_map>
#include <string>
using namespace std;

int main() {
    // 1. map 示例
    map<string, int> m;
    m["apple"] = 5;
    m["banana"] = 3;
    m["cherry"] = 8;

    for (const auto& p : m) {
        cout << p.first << ": " << p.second << endl;  // 按字母顺序输出
    }

    // 范围查询
    auto it = m.lower_bound("b");
    auto end = m.upper_bound("c");
    // it 到 end 就是 [b, c] 区间

    // 2. unordered_map 示例
    unordered_map<string, int> um;
    um["apple"] = 5;
    um["banana"] = 3;
    um["cherry"] = 8;

    // 查找极快
    if (um.find("banana") != um.end()) {
        cout << "found!\n";
    }

    // 自定义类型示例(unordered_map)
    struct Person { string name; int age; };
    struct Hash {
        size_t operator()(const Person& p) const {
            return hash<string>{}(p.name) ^ p.age;
        }
    };
    unordered_map<Person, double, Hash> salary;
}

五、总结

简单来说:

std::map = 稳定、有序、可靠(红黑树)

std::unordered_map = 极快、无序、需要精心设计 hash(哈希表)

在实际项目中,大部分的场景下 unordered_map 更快,但关键路径、需要有序或最坏情况稳定的地方,必须用 std::map。

相关推荐
故事和你911 天前
洛谷-数据结构1-1-线性表1
开发语言·数据结构·c++·算法·leetcode·动态规划·图论
脱氧核糖核酸__1 天前
LeetCode热题100——53.最大子数组和(题解+答案+要点)
数据结构·c++·算法·leetcode
脱氧核糖核酸__1 天前
LeetCode 热题100——42.接雨水(题目+题解+答案)
数据结构·c++·算法·leetcode
王老师青少年编程1 天前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:数列分段 Section I
c++·算法·编程·贪心·csp·信奥赛·线性扫描贪心
王老师青少年编程1 天前
csp信奥赛C++高频考点专项训练之贪心算法 --【线性扫描贪心】:分糖果
c++·算法·贪心算法·csp·信奥赛·线性扫描贪心·分糖果
leaves falling1 天前
C++模板进阶
开发语言·c++
无敌昊哥战神1 天前
【保姆级题解】力扣17. 电话号码的字母组合 (回溯算法经典入门) | Python/C/C++多语言详解
c语言·c++·python·算法·leetcode
脱氧核糖核酸__1 天前
LeetCode热题100——238.除了自身以外数组的乘积(题目+题解+答案)
数据结构·c++·算法·leetcode
ouliten1 天前
C++笔记:std::invoke
c++·笔记
j_xxx404_1 天前
C++算法:哈希表(简介|两数之和|判断是否互为字符重排)
数据结构·c++·算法·leetcode·蓝桥杯·力扣·散列表