C++哈希表:unordered_set / unordered_map底层原理

一、set 与 unordered_set、map 与 unordered_map 对比

set/map底层是红黑树(平衡二叉搜索树)unordered_set/unordered_map底层是哈希表(哈希桶)

1. 模板参数差异

复制代码
// unordered_set
template<class Key,
    class Hash = hash<Key>,        // 哈希函数,把key转为整型哈希值
    class Pred = equal_to<Key>,    // 相等比较函数
    class Alloc = allocator<Key>>
class unordered_set;

// set
template<class Key,
    class Compare = less<Key>,    // 小于比较函数,用于红黑树排序
    class Alloc = allocator<Key>>
class set;
复制代码
// unordered_map
template<class Key, class T,
    class Hash = hash<Key>,
    class Pred = equal_to<Key>,
    class Alloc = allocator<pair<const Key, T>>>
class unordered_map;

// map
template<class Key, class T,
    class Compare = less<Key>,
    class Alloc = allocator<pair<const Key, T>>>
class map;

|-----------------------------|-----|------------------------|--------|------------|
| 容器 | 底层 | key 要求 | 迭代器遍历 | 增删查平均效率 |
| set/map | 红黑树 | 支持less小于比较 | 有序 | O(logN |
| unordered_set/unordered_map | 哈希桶 | 支持哈希转换 + equal_to 相等比较 | 无序 | O(1 |

unordered 系列名字含义:无序,遍历顺序不按照 key 大小排序。 最坏情况哈希冲突严重时,性能退化到O(N

2. 使用上核心区别

  1. key 的要求
  • set:key 要能做小于比较,底层红黑树依靠比较完成查找插入。

  • unordered_set:key 两件事:①可以算出哈希值;②可以判断两个 key 是否相等。

  1. 迭代器
  • set 迭代器双向迭代器,遍历结果有序;

  • unordered_set 迭代器单向迭代器,遍历是无序。

  1. 性能:绝大多数场景 unordered 增删查更快;但是无序;set 可以得到有序结果。

二、哈希表基础概念

哈希表核心思想:通过哈希函数,直接把 key 映射成存储位置,实现接近 O (1) 查找

1. 直接定址法

key 范围很集中,直接拿 key 作为数组下标。 例:key 范围[0,99],直接开 100 大小数组,arr[key]存放数据。

复制代码
h(key) = key

优点:无冲突,速度极快; 缺点:key 范围分散的时候,数组空间爆炸,极度浪费内存,只适合范围集中的整型。

2. 除留余数法(最常用哈希函数)

哈希表数组大小 M,h(key)= key % M,取余数作为数组下标。

复制代码
h(key) = key % M;
  • M 是哈希表桶的个数。

  • 问题:不同 key 算出来的 h (key) 可能相同 → 哈希冲突

例如 M=16,63%16 =1531%16=15,两个不同 key 映射同一个下标,发生冲突。
经验:M 尽量取质数,不要取 2 的整数次幂,可以减少冲突。 工程上 Java HashMap 会用 2 的整数次幂,不用取模,改用位运算提高效率,属于工程优化。

3. 哈希冲突

不同 key 经过哈希函数,得到相同存储下标,叫做哈希冲突(哈希碰撞) 。 理想哈希函数完全无冲突,但现实无法做到,只能尽量降低冲突概率,冲突不可避免,需要专门方案解决冲突

方案 1:开放定址法(线性探测)

发生冲突,向后找下一个空位置存放。 公式:

最多探测 M‑1 次,一定能找到空位。

举例:M=11,插入序列{19,30,5,36,13,20,21,12}

  • h(19)=19%11=

  • h(30)=30%11= → 冲突,向后探测 i=1,位置 9

  • h(5)=

  • h(36)=36%11=

  • h(13)=13%11=

  • h(20)=20%11= →冲突,向后探测 i=1,位置 10

  • h(21)=21%11=1 →冲突,向后探测 i=1,位置 0

  • h(12)=12%11=

数组分布:

|----|----|----|----|----|---|---|---|---|----|----|----|
| 下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 数据 | 21 | 12 | 13 | 36 | | 5 | | | 19 | 30 | 20 |

开放定址法缺陷:

  1. 删除不能直接置空!直接置空会打断探测链,find找不到后面元素。只能做标记删除。

  2. 冲突堆积,聚集效应,冲突越多,探测越长,性能急剧下降。

  3. C++ 标准库unordered_set/unordered_map没有使用开放定址,使用链地址法。

方案 2:链地址法(哈希桶,STL unordered 底层实现)

每个数组下标存一个链表,哈希值相同的 key 挂在同一个链表下面。 数组的每个元素叫桶(bucket)

  • 数组下标:哈希值取模得到桶号;

  • 同一个桶内所有元素形成链表;

  • 查询:先算哈希值定位桶,再遍历桶内链表比对 key 是否相等。

当桶内链表过长,查找退化成 O (N),于是引入负载因子负载因子\\lambda = \\frac{元素个数}{桶的数量 负载因子越大,冲突概率越高,链表越长。 STL unordered 容器默认负载因子阈值为 1,当 λ>1,就会扩容:开辟更大的桶数组,把所有元素重新哈希,挂到新桶。

扩容不是简单拷贝,所有 key 要重新计算哈希映射新桶下标,叫rehash


三、unordered_set /unordered_map 工作流程

  1. 插入: ①调用 hash 函数计算 key 哈希值; ②哈希值对桶数量取模,得到桶编号; ③遍历该桶链表,如果 key 已经存在,set/map 不重复插入; ④key 不存在,头插 / 尾插挂入该桶链表; ⑤检查负载因子,超过阈值触发 rehash 扩容。

  2. 查找: ①算出 key 哈希值,取模定位桶; ②遍历桶内链表,equal_to 判断 key 相等;找到返回迭代器,找不到返回 end ()。

  3. 删除: ①定位桶; ②遍历链表找到对应节点; ③链表删除节点; ④不需要 rehash(不会缩容)。

为什么 unordered 需要 hash+equal_to 两套函数?

  • hash:快速定位到属于哪一个桶;哈希值相同不代表 key 相等(冲突)。

  • equal_to:桶内链表遍历的时候,判断两个 key真正相等

两个 key 相等,则哈希值必须相等;哈希值相等,key 不一定相等。
set/map 不需要 hash,依靠 less 比较,一边比较一边查找。

自定义类型使用 unordered_set

如果是自定义类,必须提供两个东西:

  1. 哈希函数hash<T>,把对象转为 size_t 哈希值;

  2. equal_to<T>,实现 == 运算符。

复制代码
struct Person
{
    int id;
    string name;
    bool operator==(const Person& p) const
    {
        return id == p.id && name == p.name;
    }
};

// 自定义哈希函数
namespace std
{
    template<>
    struct hash<Person>
    {
        size_t operator()(const Person& p) const
        {
            size_t h1 = hash<int>{}(p.id);
            size_t h2 = hash<string>{}(p.name);
            return h1 ^ (h2 << 1);
        }
    };
}

四、set/map vs unordered_set/unordered_map 怎么选

  1. 需要有序输出 key:选set/map,红黑树O(logN

  2. 只需要增删查,不在乎顺序,追求速度:选unordered_xxx平均O(1

  3. 注意 unordered 的最坏情况:哈希冲突严重,性能退化 O (N);

  4. unordered 迭代器单向,不支持--反向;set 双向迭代器支持++ --

五、面试高频总结

  1. unordered 底层:哈希桶(链地址法),数组 + 链表;set 底层红黑树。

  2. 哈希冲突:不同 key 哈希映射同一位置;解决:开放定址、链地址法;STL 用链地址法。

  3. 负载因子:元素数量 / 桶数量;超过阈值触发 rehash,重新分配桶数组,全部元素重新哈希。

  4. unordered 模板参数:hash 哈希函数、equal_to 相等比较;set 是 less 小于比较。

  5. 开放定址法删除不能直接置空,需要标记删除,STL 没有采用。

  6. 为什么不全部用 unordered?无序、最坏性能退化、自定义类型需要提供哈希函数。

相关推荐
在所不辞兄15 分钟前
【零基础学智能仿真-06】决策树——让模型自动发现力学失效阈值
算法·决策树·机器学习
啥都想学点的研究生31 分钟前
一篇文章讲清楚:C4.5决策树和CART决策树
算法·决策树·机器学习
DDXYcoder32 分钟前
万字解析动态内存从编译到运行
java·前端·算法
阳明山水36 分钟前
概念漂移分类与自适应策略解析
人工智能·深度学习·算法·机器学习·架构
OPEN-F41 分钟前
C++工程化:编译链接、调试与性能优化
开发语言·c++
欧特克_Glodon1 小时前
OpenCV计算机视觉开发入门与实践<二十九>:grabCut算法分割图像
opencv·算法·计算机视觉·图像分割·grabcut 算法
邪修king1 小时前
Re:Linux 系统篇(十七):进程篇(六):环境变量深度解析|命令行参数、环境变量表、4 种获取方式与继承机制万字详解
linux·服务器·c++
方方洛1 小时前
vllm教程-02-核心原理
人工智能·算法·vllm
张小姐的猫1 小时前
【C++开发脚手架】 —— Jsoncpp上手
java·linux·开发语言·网络·c++·tcp/ip·udp