从哈希函数出发,谈一谈如何处理海量数据
海量数据处理
给出如下的一个场景:
有40亿个(数据规模足够大,内存大小限制放不下)不重复的无符号整数(>=0)设计,一种算法 or 数据结构满足以下需求。
- 40亿个数据基本固定
- 经常进行判断一个数字n在不在集合中
**现在我们先设想如果不是海量数据,我们可以怎么做?**常规解决思路就是查找搜索
- 遍历查找
- 排序后二分查找
- 放入Set中 (快 但是需要额外的空间)
- 自己实现一种直接地址法的哈希表 (要求就是数据比较连续)
为什么自己实现直接地址法的哈希表要求数据要连续呢?
如果数据规模不大,但是数据不连续,使用直接地址法的哈希表会有下面的一些问题:
虽然可以解决冲突问题,但是缺点很明显。哈希数组的长度是和数字的最大值有关,而不是集合数字的规模,如果只有两个数字,一个1,一个二十万,缺点显而易见。
如果是海量数据,上述的各种做法就不太好。数据太大也就说,内存放不下,只能放到硬盘里面。内存的数据可以随机访问,但是硬盘的就不可以(磁道寻址的问题)。
位图结构 bitmap
(缩小存储空间)
第一种思路:还是上面的直接地址法的哈希,如果是40亿个数字,还是上面的做法,需要40亿个 int 占用的空间 。解决的思路就是:缩小每个数据占用的空间,进而使得整个集合占用的空间减少。
要表示存在或者不存在,本质上是一个二元信息,理论上,只需要一个bit就足够保存数据了。 40亿 *1bit - > 使得整体的空间占用小,使得可以在内存中解决
由于海量数据的特点导致集合的空间占用太大,减少每个元素的占用的空间,由于只判断在与不再,二元信息所以是1个比特即可。
由于bit数组不存在 ,所以需要自己构造.
那么具体数据的操作,该如何利用比特位实现:
建立一个byte[] 类型的数组,数组的 元素类型是byte。 byte 类型是一个字节,也就是可以用8个比特位
0 表示不存在 1表示存在
x/8 计算出在哪一个字节 x%8 计算出这个字节的哪一个比特位
了解了大概的原理,如何实现呢?
位图使用
首先根据计算 x/8 计算出来 了要对数组的哪一个index进行操作。
那么如何对这个byte[Index] 的 某一个bite 位进行操作呢?
假设 int bit = x %8
添加x
首先定位到byte[index] ,然后更改对应的bite位为1
(注意一个坑点,不可以直接对原来的数据进行移位操作,移位会改变原有数据)
复习一下位操作
可以借助对1 的移位进行操作 原来的byte[index ] | (1 << bite)
这样就可以仅仅改变bite 这1个比特位
原来是1 不变
原来是0 变为1
删除x
byte[index ] & ( ~ (1 << bite))
不论原来这个位置是0 还是1 都改为 0 即可
所以按位与0 就可以了。
判断x是否存在
byte[index ] & (1 << bite)
结果是0 说明不存在,反之,存在
位图面试题
给定100亿个整数,设计算法找到只出现一次的整数?
存在或者不存在,可以通过位图解决。(二元信息,一个bit存储)
但是找到只出现一次的现在需要
- 不出现
- 只出现一次
- 出现一次以上
那么就是一个三元的信息,一个bit放不下,那就用两个bit来做。两个bit可以最多保存四元信。所以需要设计一下编码的格式,同时需要使用两个位图。
但是由于找的是原来的那个数字,又由于位图本身是不能存在数字元素的信息,所以需要根据下标在反推会元素,这也就要求不可以有hash冲突
-
先确定是几元信息 - 三元信息
-
根据几元信息确定需要几位存储 - 2bit存储
-
为各种情况进行编码 - 考虑到计算的方便性
0 0 表示一次都没有出现
0 1 表示出现一次
1 1 表示出现一次以上
1 0 弃用 (因为设计的时候 采用得到 两个位的异或关系,要确保只出现一次的位图异或的结果是1 ) -
处理 遍历所有数字,记录数字的出现次数
然后遍历位图的每一位 找出出现一次的 -
还原成对应的数字 (还原数字的时候 只用将上面计算在哪一个byte[index] 的哪一个bite 反过来即可!)
给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
- 确定几元信息
两个文件 只有可能有四种情况(四元信息)
- 没有出现在A 也没有出现在B
- 只出现在A
- 只出现在B
- 出现在A和B中
-
需要2个比特存储
-
编码设计
-
处理 找到 两个位图中为止按位与是1的即为交集。
-
还原成对应的数字
另一种解决思路 用一个位图搞定
可以先把第一个文件哈希放到一个位图里面去,存在设置为1 即可。
然后遍历第二个文件,也哈希,如果遇到了1,就说明找了交集。
位图总结
问题的特点
- 海量
- 无符号整数(>=0)
- 不能重复 (这点很重要!)
- 只能处理存在与否的问题 不能计算个数
使用场景
- 只适合在一个集合中判断元素存在与否的问题(exists)
- 排序 + 去重
- 求两个集合的交集、并集等
- 操作系统中磁盘块标记
布隆过滤器 Bloom Filter
特点
检查元素是否在集合中存在并且集合中的数据规模特别大
使用场景
上述位图的限制 只能是数字,不能重复,只能判断是否存在的问题,也有很多的限制。
举例一:新闻
元素:新闻
集合:已经给你推送过的新闻视频等
由于不允许重复,所以,每次需要检查新闻是否被推送过
检查元素是否在集合中存在
显然由于不是数字,无法使用位图的方式
举例二:作弊判定
作弊名单比较大
判断这个玩家是否之前作弊。也就是检查(元素)玩家是否有在(集合)作弊名单中(很多人)存在
布隆过滤器的内部结构
是n个哈希函数和位图的结合
- 一个元素放入结合中
- 把元素分别经过n个hash函数的计算,并转化为位图上的合法下标
- 为所有下标处“位图”位置改为1
- 删除一个元素
布隆过滤器无法实现删除的。
因为得到的合法下标的1不仅仅放着当前需要删除元素的信息,还和别的元素有关系
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给k个计数器(k个哈
希函数计算出的哈希地址)加一,删除元素时,给k个计数器减一,通过多占用几倍存储空间的代价来增加删
除操作。
缺陷:
-
无法确认元素是否真正在布隆过滤器中
-
存在计数回绕
-
判断一个元素是否在集合中
- 把元素分别经过n个哈希函数进行计算,并转换为位图上的合法下标
所有位置有0 ,则表示元素不在集合中
所有位置都是1,则大概率元素在集合中
布隆过滤器总结
阳性:判定元素在集合中
阴性:判定元素不再集合中
布隆过滤器出现阴性结果,一定是准确的。但是出现阳性结果,则有概率出现假阳性的情况。即一定可以判断元素不在集合中,大概率可判断元素在集合中(概率可以通过增加hash函数的个数,有效的减少)虽有有假阳性,但是假阳性的概率不高,还是有实用性的。(宁可错杀,不可放过)
布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可能存在,因为有些哈希函数存在一定的误判。
多个hash函数的原因:(哈希冲突的必然性)
元素由于不限,必然会产生hash冲突的情况(不同的元素,映射到相同的下标)
在这种情况下,如果hash函数太少,假阳性概率特别高。通过增加hash函数的个数,会使得假阳性概率下降。
布隆过滤器的优点
- 增加和删除元素的时间复杂度:O(k),k是哈希函数的个数,(一般比较小)与数据量无关。
- 哈希函数之间没有关系,方便硬件进行并行运算
- 布隆过滤器本身不需要存储元素,对保密要求比较严格的场合有较大的优势
- 在能够承受一定的误判时,布隆过滤器比其他数据结构有更大的空间优势
- 使用同一组散列函数的布隆过滤器可以进行交、并、补、差运算
- 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
布隆过滤器的缺点
- 有误判率,即存在假阳性(False Position),即不能准确判断元素是否在集合中(补救方法:再建立一个白
名单,存储可能会误判的数据) - 不能获取元素本身
- 一般情况下不能从布隆过滤器中删除元素
- 如果采用计数方式删除,可能会存在计数回绕问题
实际场景举例:
比如有10亿个玩家,然后进行游戏行为判定找到了1万个人作弊(这里面可能会存在着没有作弊却被封号了),然后假阳性的玩家找过来了,那么可以使用更精确的的算法进行玩家作弊判定。(1万个相对来说已经不是海量数据了)
- 如果该玩家确实作弊 那就是阳性
- 如果没有作弊 解封补偿玩家
布隆过滤器面试题
给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出精确算法和近似算法
处理利用上面的位图确定几元信息,还可以使用布隆过滤器
使用hash函数将第一个文件的所有整数映射到1000个文件中,每个文件有1000万个整数,大约40M内存,
内存可以放下
把1000个文件记为 a1,a2,a3…a1000
用同样的hash函数映射第二个文件到1000个文件中,这1000个文件记为b1,b2,b3…b1000,
由于使用的是相同的hash函数,所以两个文件中一样的数字会被分配到文件下标一致的文件中,分别对a1和b1求交集,a2和b2求交集,ai和bi求交集,最后将结果汇总,即为两个文件的交集
哈希切割
给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?
- 海量数据
- 找出现次数,不是存在与否问题
如果把数据进行文件的拆分,拆分为多个不同的文件,然后把每一个文件里面的ip出现次数最多的拿出来,再次进行一个比较。
但是这样的做法是不可行的,无法保证相同的ip出现在相同的文件中进行计数
那么又如何做到相同的ip分到相同的组。-- 哈希
那么海量数据处理的第二种思路,分组处理,其中分组的方式采用哈希分组(哈希函数可以灵活设计)
只需要保证ip相同的在一组,没有必要说一组里面只出现一个ip,是可以出现哈希冲突的。
经过哈希函数处理之后,文件可能会被划分长下面的样子
与上题条件相同,如何找到top K的IP?
5TB的访问日志,如何找到访问次数最多的10个ip?
方案一:
- 统计每一个ip的出现次数,得到如下的集合:
[ip - 次数 , ip- 次数,…]
依次处理,内存够大放内存处理,内存不够大,就去读取文件处理。并没有海量数据的限制。
处理过程中数据量的大小和ip有多少个不同的有关系。 - 直接创建一个小堆(堆中可以保存10个元素)。堆中存放的元素是 ip - 次数,评判标准是次数。
- 遍历第一步得到的集合,然后依次和小堆的堆顶元素比较。如果比对顶元素小,那么就跳过。否则就替换堆顶,然后向下调整堆。
- 最后堆保留的就是出现次数最多的K个元素。
(为什么不可以建立大根堆)
每一个划分的子文件都用topK建立一个大根堆,然后将所用的子文件放在一起
方案二: 使用hash分组的方式,分为多个文件
划分文件n个,每个文件都得出topK,将n个topK进行再次的topK得出结果即可。
方案一和方案二对比:
第一种只能用线性的方式去解决,速度不快。
但是第二中方案由于划分文件了,可以并行去各自处理,最后将结果在一起合并找出。
总结
总结海量数据处理的两类思路办法
1、 压缩信息的办法:位图、布隆过滤器
2、分文件处理,一般要采用哈希分组的方式,保证相同的数据分到同一组(这种思想可以作为通用的办法)
海量数据处理的面试题,一般通过 画图+口头描述+部分伪代码的方式,把过程讲解清楚即可。重点部分突出。