C++哈希表设计

unordered****系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器map、multimap、set、multiset,在查询时效率可达到 log2n,即最差情况下需要比较红黑树的高度次,我们想要以O(1)的时间复杂度进行查找,于是C++11中,STL又提供了4个 unordered系列的关联式容器unordered_map、unordered_multimap、unordered_set、unordered_multiset,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍

unordered_map

  1. unordered_map存储的元素是<key, value>,其允许通过key键值 以O(1)的时间复杂度查找到对应元素。
  2. 在unordered_map中,键值key用于唯一地标识元素,映射值是value,共同组成了元素<key,value>,键和映射值的类型可以相同可以不同。
  3. unordered_map没有对<key, value>元素按照特定的顺序排序, 而是将键值对应的哈希值相同的元素放在同一个桶中,一个哈希值(哈希地址)对应一个桶,桶的本质就是一个链表,哈希表中用顺序表来记录所有链表的头结点。
  4. unordered_map容器通过key键值访问元素要比map快
  5. unordered_map实现了operator[ ],该函数是一个插入查询操作,它会尝试insert插入<key, V()>,如果键值key的元素已经存在,那就插入失败,返回已存在元素的迭代器和false,如果键值key的元素不存在,那就插入成功,返回新插入元素的迭代器和true,operator[ ]会使用迭代器解引用得到元素,然后返回映射值value的引用
  6. 它的迭代器至少是前向迭代器,也就是说迭代器至少要支持++,如果迭代器支持了--,那就是双向迭代器了。

unordered_map的操作

unordered_map的定义

第一个模板参数是键值类型,第二个模板参数是映射值类型,这就是unordered_map的元素的键值和映射值类型,第三个模板参数是仿函数类,可以将键值计算为对应哈希值,第四个参数忽略,第五个参数空间配置器类型

empty

如果unordered_map中没有元素就返回true,如果有就返回false

size

返回unordered_map中元素个数

begin

unordered_map的迭代器其实是复用了哈希表的迭代器,返回一个哈希迭代器,其结点指针指向哈希表中第一个不为空的桶的头结点

end

返回一个unordered_map迭代器, 即哈希表迭代器,其结点指针为nullptr

operator[ ]

该函数是一个插入查询函数,返回键值k对应的映射值value的引用

其函数的行为是:(insert({k, V( )}).first)->second

如果键值为k的元素已存在,那就插入失败,返回已存在元素的迭代器和false,如果键值为k的元素不存在,那就插入成功,返回新插入元素的迭代器和true,operator[ ]使用迭代器,返回元素中映射值的引用

find

如果键值为k的元素存在,那就返回迭代器指向该元素的哈希节点,如果键值为k的元素不存在,那就返回end(),即迭代器中的哈希节点指针是nullptr

count

返回键值为k的元素的个数,因为unordered_map中不允许相同键值元素存在,所以count的返回值要么是0,要么是1

insert

val是要插入的元素,unordered_map中的元素类型是<Key, Value>,如果键值相同的元素已经存在,那就返回已存在元素的迭代器和false,插入失败,如果该键值的元素不存在,那就插入成功,返回新插入元素的迭代器和true,插入成功

erase

如果用迭代器作为参数来删除元素,那么会删除对应元素并返回删除元素下个位置的迭代器,如果删除节点后面还有下一个节点,那就返回下个节点的迭代器,如果删除节点没有下个节点,那就返回下个不为空的桶的头结点,如果后面没有不为空的桶,那就返回end()

clear

清空unordered_map中的元素

bucket_count

返回桶的数量,也就是哈希值的数量

bucket_size

返回哈希值n对应的桶里面的的元素数量,哈希值的范围是【0,bucket_count-1 】

swap

交换两个unordered_map的成员变量,即维护哈希桶的线性表和元素个数两个成员

unoedered_map模拟实现

unordered_map模拟实现

unordered_set

unorderd_set只有底层元素和接口设计和unordered_map不一样,其他的思路都一样,unordered_set的元素是<T, T>,可以理解为<key, key>或<value, value>。同样不允许有相同键值的元素存在

底层结构

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希表

哈希概念

通过哈希函数将键值转换为哈希值,将元素保存在哈希值对应的位置上,这样构造出的结构称为哈希表。哈希表中查找元素时只需将元素的键值转换成对应哈希值,然后在哈希值对应的位置进行查找即可,因此查找元素的时间复杂度为O(1)

哈希冲突

假如两个元素的键值不相同,但是经哈希函数计算得到的哈希值相同,我们称这种情况为哈希冲突或哈希碰撞

常见哈希函数

  1. 直接定址法
    取键值的某个线性函数为哈希函数,哈希值 HashKey= A*Key + B

  2. 除留余数法
    设哈希表中的哈希地址个数为m,将m作为除数,按照哈希函数:HashKey = key% m,将键值转换成哈希值
    注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

哈希冲突解决方法

解决哈希冲突两种常见的方法是:闭散列法和开散列法,闭散列法又叫开放定址法,开散列法又叫做链地址法或哈希桶法。

闭散列法

闭散列法也叫开放定址法,当发生哈希冲突时,将元素放到后面其他哈希值的位置,至于如何找这个位置,一般有两种方法,一个是线性探测法,一个是二次探测法

线性探测

从发生冲突的哈希地址开始,依次向后探测,直到寻找到下一个空位置为止。

二次探测

线性探测在冲突哈希地址的基础上每次加一个递增的偏移量,就是HashKey+i,i 取1~n,比如冲突的哈希地址为5,那下次的位置就是6、7、8....。而二次探测则是在冲突哈希地址的基础上每次加一个平方,就是 HashKey+i^2,i 取1~n,比如冲突的哈希地址为5,那下次的位置就是6、9、14......

**注意:**由于闭散列插入元素是当发生哈希冲突时,往冲突地址后面根据线性探测或二次探测的方法找第一个空位置然后插入,所以在进行查找时,会从哈希地址开始,然后根据插入元素时的线性探测或二次探测方法来查找元素,如果碰到空位置,说明元素不存在,如果在碰到空位置之前找到了元素,那就是存在该元素。在删除元素时,我们不能物理删除,因为这样的话会出现一个空位置,影响后面元素的查找,因此对于闭散列的哈希表实现,我们要给元素加上状态标志,分别是EMPTY、EXIST、DELETE

闭散列的哈希表实例:

依次插入1、4、5、6、7、9,都没有发生冲突,插入44,哈希值为4发生冲突,线性探测找到第一个空位置是哈希地址为8的位置,于是将44插入到8

哈希表的扩容

哈希表的载荷因子定义为元素个数和哈希地址个数的比值,当载荷因子过大时,往哈希表中插入元素时发生哈希冲突的概率就会很大,而且无论是使用闭散列还是开散列方法解决了哈希冲突,后面查找速度都会变慢,所以当载荷因子达到一个阈值时我们就对哈希表进行扩容,也就是增加哈希地址的个数

开散列法

开散列法又叫链地址法或哈希桶法,开散列法的哈希表对于每一个哈希地址都会维护一个链表,发生哈希冲突的元素通过头插的方式加入到同一个链表中,我们形象地称一个链表为一个桶,哈希表中会用线性表来记录每个桶的头结点指针
开散列法的哈希表实例


从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

开散列与闭散列比较

使用链地址法处理哈希冲突,需要给元素增设链接指针,而开放定址法必须保持大量的空闲空间以确保效率,元素所占空间又比指针大的多,所以使用链地址法比开放定址法节省空间。

哈希表的实现

哈希表模拟

哈希的应用

位图

位图使用bit位来表示信息,常用于海量数据的场景

案例:
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
这40亿个数中。【腾讯】
**解:**无符号数的个数是2^32,创建一个2^32个bit位的位图,用来表示每一个无符号整数是否存在,所占空间512MB。遍历一遍40亿个数,设置好位图中对应位,然后后面判断一个数是否存在只需要判断对应比特位是否是1即可

bitset

N是一个非类型模板参数,其值为bitset中的有效bit位数,bitset在底层开辟空间时,是一次开辟一个int也就是32个bit位,所以如果N不是32的倍数,那就会有浪费,N表示的不是开辟空间的bit位数,而是有效bit位数

bitset的操作

operator()

返回下标为pos的bit位的值,该函数没有有效字符范围检查

count

返回bitset中被设置为1的有效bit的个数

size

返回bitset中有效bit的个数

test

返回下标为pos的bit位的值,该函数和operator[ ]相比,有这个有效bit的范围检查

any

检查bitset中是否有任何一个bit位为1,如果有那就返回1,如果没有就返回0

none

检查bitset中是否所有位都没有被设置,如果都没被设置,那就返回true,否则false

set

第一个函数会将bitset中的所有有效位设置为1,第二个函数会将bitset中指定下标的bit位设置为指定值val

reset

第一个函数会将bitset中的所有位设置为0,第二个函数会将bitset中下标为pos的位设置为0

布隆过滤器

布隆过滤器是用来查找一个元素是否存在的,布隆过滤器由一个位图和多个哈希函数构成,一个元素的键值可以通过多个哈希函数计算出多个哈希值,然后设置位图中的多个不同位,基于这种特性,我们在进行查找时,如果缺少一个位没有被设置,那这个元素一定不存在,如果所有位都被设置,则元素可能存在,因为可能是由于其他元素存在导致该位被设置所以只能是可能存在

布隆过滤器的查找

对元素的键值分别使用不同哈希函数计算每个哈希值,然后查看对应的位置bit位的是否为零,只要有一个为零,代表该元素一定不存在,如果每个哈希值对应的bit位都被设置了,那该元素就有可能存在

布隆过滤器删除

布隆过滤器不支持删除工作,因为在删除一个元素时,可能会影响其他元素的查找。如果要支持布隆过滤器的删除功能,则每个哈希值在位图中对应的就不能是单单一个bit位,而是多个bit位,也就是该哈希值的映射计数。当我们删除一个元素时,只需要将这个元素的键值对应的哈希值对应的计数器减一即可

布隆过滤器优点

  1. 增加和查询元素的时间复杂度为:O(K), (K为哈希函数的个数,一般比较小),与数据量大小无
  2. 哈希函数相互之间没有关系,方便硬件并行运算
  3. 布隆过滤器不存储元素本身,在某些对保密要求比较严格的场合有很大优势
  4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有很大的空间优势
    5、如果数据不存在时,一定是准确的

布隆过滤器缺陷
1.元素存在的结果是不可信的
2. 只能判断元素是否存在,不能获取元素本身
3. 一般来说布隆过滤器不支持删除

海量数据面试题

哈希切割

给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
与上题条件相同,如何找到top K的IP?
如何找出现次数最多的ip地址?
使用哈希函数将100G的大文件划分成多个小文件,相同ip肯定会被分到同一个小文件中,然后先处理一个小文件,用堆来记录小文件的ip个数情况,留下堆顶那个个数最多的ip,然后再去处理下一个小文件,最后将每个小文件中个数最多的ip汇总到一块建个堆,得出大文件中个数最多的ip
如何找出top K的ip地址?
使用哈希函数将100G的大文件划分成多个小文件,相同ip肯定会被分到同一个小文件中,然后先处理一个小文件,用堆来记录小文件的ip个数情况,留下堆中top k个个数最多的ip,然后再去处理下一个小文件,最后将每个小文件中top k个数最多的ip汇总到一块建个堆,得出大文件中个数最多的top K的ip

位图应用

  1. 给定100亿个整数,设计算法找到只出现一次的整数?
    整数按四字节来算,那一共就有2^32个不同的整数,我们设计一个位图,每个整数对应两个位,可表示四种状态,0就代表一次没有出现,1表示出现1次,2表示出现2次,那这个位图就是1GB,然后遍历100亿个整数,最后看哪些整数出现是一次
  2. 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
    整数的个数一共是2^32个,创建一个位图,一个整数对应一个位,位图的大小是512MB,遍历第一个100亿整数的文件,设置对应的位,然后再创建一个位图,同样一个整数对应一个位,大小是512MB,遍历第二个100亿数据文件,然后根据第一个位图,将并集结果保存到第二个位图
相关推荐
Fighting_p7 小时前
【导出】前端 js 导出下载文件时,文件名前后带下划线问题
开发语言·前端·javascript
JIngJaneIL7 小时前
基于java+ vue畅游游戏销售管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·游戏
春日见7 小时前
用matlab对相机进行标定获取相机内参
开发语言·数码相机·matlab
while(1){yan}7 小时前
HTTP的加密过程
java·开发语言·网络·网络协议·http·青少年编程
guygg888 小时前
一维移动最小二乘近似的MATLAB程序
开发语言·matlab
一分之二~8 小时前
回溯算法--递增子序列
开发语言·数据结构·算法·leetcode
艾莉丝努力练剑8 小时前
【Python库和代码案例:第一课】Python 标准库与第三方库实战指南:从日期处理到 Excel 操作
java·服务器·开发语言·人工智能·python·pycharm·pip
Yu_Lijing8 小时前
基于C++的《Head First设计模式》笔记——策略模式
c++·笔记·设计模式
yugi9878388 小时前
基于C#实现的WiFi信号强度扫描程序
开发语言·c#