unordered_map、unordered_set底层哈希表的实现机理

unordered_map、unordered_set底层哈希表的实现机理

  • 哈希表
    • 哈希函数
    • 著名的hash算法
    • 各种字符串hash函数
    • 哈希冲突
  • unordered_map、unordered_set的底层机理
    • unordered_map、unordered_set的底层原理
    • hashtable
  • map和unordered_map的对比

哈希表

  • 数组的特点是:寻址(查询)容易,插入和删除困难;

  • 链表的特点是:寻址(查询)困难,插入和删除容易。

  • 那么我们能不能综合两者的特性,做出一种寻址(查询)容易,插入删除也容易的数据结构,答案是肯定的,这就是我们要提起的哈希表 。
    hash函数就是根据key计算出应该存储地址的位置,地址index=H(key),而哈希表是基于哈希函数建立的一种查找表。

哈希函数

  • 哈希函数的四个性质:
  1. 输入域是无穷的,输出域相对有限

  2. 相同的输入值,一定输出相同的返回值

  3. 不同的输入值,也可能输出相同的返回值(冲突)

  4. 不同的输入,如果想得到整个输出域上的返回值,则整个输出域上的返回值是均匀分布的。举例:input[0,1,2…99], 经过哈希函数得到output[0,1,2],则映射到0,1,2的输入值个数大概是33,33,33。即均匀分布。同时对output输出的值域mod M后值域变成[0,1 2…M-1],在新的值域上也是均匀分布的。

  • hash函数设计的考虑因素
    1.计算散列地址所需要的时间(即hash函数本身不要太复杂)
    2.关键字的长度
    3.表长
    4.关键字分布是否均匀,是否有规律可循
    5.设计的hash函数在满足以上条件的情况下尽量减少冲突

  • 哈希函数的构造方法
    根据前人经验,统计出如下几种常用hash函数的构造方法:

  1. 直接定制法
    哈希函数为关键字的线性函数如 H(key)=a*key+b

这种构造方法优点比较简便,均匀,
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况

  1. 除留余数法
    H(key)=key MOD p (p<=m m为表长)

很明显,如何选取p是个关键问题。
使用举例
比如我们存储3 6 9,那么p就不能取3
因为 3 MOD 3 == 6 MOD 3 == 9 MOD 3
p应为不大于m的质数或是不含20以下的质因子的合数,这样可以减少地址的重复(冲突)

  1. 平方取中法

假设关键字为5432,对它平方就是29506624,抽取中间的3位506(或066)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

  1. 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

  1. 随机数法
    H(key) = random(key),其中random为随机数函数。

选择一个随机函数,取关键字的随机函数值为它的哈希地址
通常应用于关键字长度不等时采用此法

  1. 数学分析法

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀的情况

著名的hash算法

MD5和SHA-1可以说是应用最广泛的Hash算法

各种字符串hash函数

https://www.cnblogs.com/-clq/archive/2012/05/31/2528153.html

哈希冲突

即不同key值产生相同的地址,H(key1)=H(key2)。
比如我们上面除留余数法作为哈希函数时,存储3 6 9,p取3时
3 MOD 3 == 6 MOD 3 == 9 MOD 3
此时3 6 9都发生了hash冲突

  • 哈希冲突的解决方案
    不管hash函数设计的如何巧妙,总会有特殊的key导致hash冲突,特别是对动态查找表来说。hash函数解决冲突的方法有以下几个常用的方法
  1. 开放定址法

当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?

a. 线性探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

b. 二次探测
二次探测为了避免线性探测中找空位置的时挨着往后逐个去找导致数据堆积的问题。
把找下一个空位置的方法改为: H i = ( H 0 + i 2 ) % m , ( i = 1 , 2 , 3... ) H_i =(H_0+i^2)\%m ,(i=1,2,3...) Hi=(H0+i2)%m,(i=1,2,3...)
或者 H i = ( H 0 − i 2 ) % m , ( i = 1 , 2 , 3... ) H_i=(H_0-i^2)\%m ,(i=1,2,3...) Hi=(H0i2)%m,(i=1,2,3...)
其中H_0是通过哈希函数Hash(key)计算得到的。

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。

装载因子load factor = 哈希表的大小 / 有效元素的个数,负载因子越小,出现哈希冲突的概率越低,哈希表的效率越高,但是浪费的空间越多

  1. 链地址法(重点)
    链地址法又叫开链法/哈希桶/拉链法,
    首先对key用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。

unordered_map、unordered_set底层哈希表的实现机理_第1张图片

  1. 再散列法
    准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数,第二个也冲突,使用第三个……

  2. 公共溢出区法
    建立一个特殊存储空间,专门存放冲突的数据。此种方法适用于数据和冲突较少的情况。

unordered_map、unordered_set的底层机理

参考:
c++ 中 unordered_map 的实现

封装底层为哈希表的unordered_map/set

对 c++ unordered_map 源码的解析

https://www.journaldev.com/35238/hash-table-in-c-plus-plus

unordered_map、unordered_set的底层原理

STL中的unordered_map、unordered_set的底层使用hashtable+buket的实现原理。

  • gcc中的unordered_map.h
    https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/unordered_map.h

  • gcc中的unordered_set.h
    https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/unordered_set.h

  • unordered_map与hash_map的不同
    Difference between hash_map and unordered_map?
    由于在C++标准库中没有定义散列表hash_map,标准库的不同实现者将提供一个通常名为hash_map的非标准散列表。因为这些实现不是遵循标准编写的,所以它们在功能和性能保证上都有微妙的差别。
    从C++11开始,哈希表实现已添加到C++标准库标准。决定对类使用备用名称,以防止与这些非标准实现的冲突,并防止在其代码中有hash_table的开发人员无意中使用新类。
    所选择的备用名称是unordered_map,它更具描述性,因为它暗示了类的映射接口和其元素的无序性质。
    可见hash_map , unordered_map本质是一样的,只不过 unordered_map被纳入了C++标准库标准。
    C++ STL中哈希表 hash_map从头到尾详细介绍

hashtable

hashtable可以看作是一个数组 或者vector之类的连续内存存储结构(可以通过下标来快速定位时间复杂度为O(1),如下图的buckets vector。buckets vector中的每个元素是一个指针__hashtable_node*,
处理hash冲突的方法就是采用链地址法。

  • gcc中的hashtable.h
    https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/hashtable.h

  • STL源码剖析-hashtable
    https://blog.csdn.net/haluoluo211/article/details/80877353

  • 什么时候需要重新分配buckets vector

  • 标准库中的哈希函数std::hash
    https://en.cppreference.com/w/cpp/utility/hash

template< class Key >
    struct hash;

模板特化部分

template<> struct hash<bool>;
template<> struct hash<char>;
template<> struct hash<signed char>;
template<> struct hash<unsigned char>;
template<> struct hash<char8_t>;        // C++20
template<> struct hash<char16_t>;
template<> struct hash<char32_t>;
template<> struct hash<wchar_t>;
template<> struct hash<short>;
template<> struct hash<unsigned short>;
template<> struct hash<int>;
template<> struct hash<unsigned int>;
template<> struct hash<long>;
template<> struct hash<long long>;
template<> struct hash<unsigned long>;
template<> struct hash<unsigned long long>;
template<> struct hash<float>;
template<> struct hash<double>;
template<> struct hash<long double>;
template<> struct hash<std::nullptr_t>; // C++17
template< class T > struct hash<T*>;

map和unordered_map的对比

比较好的对比见:
https://stackoverflow.com/questions/13799593/how-to-choose-between-map-and-unordered-map/13799886#13799886
主要是,查询、插入、删除的时间复杂度三个方面

你可能感兴趣的:(C++之STL标准模板库,散列表,哈希算法)