位图与布隆过滤器

888 阅读4分钟

业务开发过程中,需要对某个对象进行标记或记录对象某个状态时,只要插入和读取两个操作,常用散列表、红黑树等数据结构记录。当记录数据量非常庞大时,消耗的内存会非常大,存储效率和查询效率也会非常低。如使用散列表记录1亿用户今日是否访问过,使用int存储用户id,需要381MB内存空间(4bytes100000000/1024/1024)。

每一个状态值使用4个字节存储会浪费很多内存,用户是否访问过,最少使用一个bit就可以表示。使用一个bit代表一个用户,1亿用户最小需要1亿bit,合记为11MB,大大减少了内存占用。

位图

位图可以在不占用很多内存的前提下,解决海量数据的存在性问题。

位图法的原理主要就是利用int类型数据,一个int类型数据是4个字节,一个字节8位,然后一个int数据利用自身字节位就可以表示0-31的数是否存在,bit位表示数值,0,1值表示这个数值是否存在。

如果要查找总数为 N 的数据存在性,只需要申请一个int 数组长度为int tmp[N/32+1]即可完成这些数据的存储。其中每个元素可标识32个数据的状态。

image.png

查找某一位的位置与状态

  1. 判断数字放在哪一个 tmp 数组中: 将数字直接除以 32 取整数部分 (x/32) ,例如:整数 8 除以 32 取整等于 0 ,那么 8 就在 tmp[0] 上;
  2. 确定数字放在32个位中的哪个位: 将数字mod32取模 (x%32) ,得到余数,即数字在一个元素中第几位。
  3. 查询此位状态: 将目标位右移至所在元素最右侧(即右移所在元素 x%32 位),与1 进行与运算,即可得到此位状态。更改状态可以通过类似操作实现。

对于多次出现的数据处理方法

然后我们怎么统计只出现一次的数呢?每一个数出现的情况我们可以分为三种:0次、1次、大于1次。也就是说我们需要用2个bit位才能表示每个数的出现情况。此时则三种情况分别对应的bit位表示是:00、01、11

我们顺序扫描这10亿的数,在对应的双bit位上标记该数出现的次数。最后取出所有双bit位为01的int型数就可以了。

优点:

  1. 节省内存空间
  2. 插入和查询时间复杂度都为O(1)O(1)

缺点:

  1. 存储数据不能重复
  2. 只能处理正整数的数据
  3. 对数据要求比较高,需要数据密度高时才能体现出高的空间效率

布隆过滤器(Bloom Filter)

布隆过滤器解决了位图在存储数据密度低时低效的问题。

布隆过滤器就是通过将元素进行多个Hash算法计算,都存入位图中,查询时使用同样的Hash算法计算,对应当所有值都为true时,表示存在。这样就可以极大的提升位图的存储效率。

💡 针对黑名单机制,布隆过滤器可以发挥很好的作用。

image.png

布隆过滤器也有致命的缺陷,即存在误判率,也称为假阳性率。当数据量不断增大,位图中非true位置越来越少,很可能会出现未插入的数据,查询结果为true。所以布隆过滤器的特点就是 False is always false. True is maybe true。因为误判率的存在,布隆过滤器多用于黑名单校验。

关于布隆过滤器准确率的计算

整个过程一共有两个未知数,使用 kk 个哈希函数,以及取模 mm 所构造的哈希表的范围。

nn 为样本量,pp 为准确率。

m=nlnp(ln2)2m=-\frac{n\ln{p}}{(\ln{2})^2}
k=ln2mn0.7mnk=\ln{2}\cdot \frac{m}{n}\approx0.7\cdot \frac{m}{n}

💡 注意: kk 向上取整

ptrue=(1enkm)kp_{true}=(1-e^{-\frac{nk}{m}})^k

优点:

  1. 提高了位图的存储效率
  2. 与散列表相比没有链表遍历,主要耗时为Hash计算,理论上时间效率更高

缺点:

  1. 存在误判率
  2. 无法删除数据

应用

  • 黑名单校验
  • 位图排序(桶排序)
  • 快速去重
  • ConsistencyCheck
  • 爬虫URL校验
  • 解决缓存穿透问题

位图高效的空间利用率只有在数据密集的情况下才能体现出来。 如:用户每日首次进入时弹窗广告,用户ID从10亿至20亿,用Integer存储。假设每日访问用户为n,存储共消耗32n个bit;用位图存储需要10亿个bit。所以当每日访问量在3125万人时,位图的存储效率才会超过列表存储。