跟我来看看BitMap怎么用-(1)

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

1 BitMap原理

在计算机中数据的基本储存单位是,它只能存储0或1,编程语言中如字符串、数字等数据在计算机中都是通过多个0或1组合在一起表示。8位为1B(字节),1024个字节为1KB,1024KB为1M。如果能利用每个位上的0或1来表示1条信息,那么1M(8388608位)就能存储非常多的内容。

BitMap就是一种记录大量0-1状态的数据结构,它采用1bit为单位进行存储key对应的value。比如我们有一块长度为1字节的内存空间,则初始状态对应的BitMap如下所示:

0 0 0 0 0 0 0 0
7 6 5 4 3 2 1 0

1字节即长度为8的BitMap,下标对应0-7的8个数字,每一bit位初始值都为0,此时我们依次插入数据2,4,6,BitMap状态变更如下:

0 1 0 1 0 1 0 0
7 6 5 4 3 2 1 0

如上所示,1代表存在,0代表不存在,bitmap由00000000变为01010100就表示这个bitmap中存在2,4,6。通过这种方式,不仅方便查询bitmap中存储哪些元素,而且可以去除掉重复元素。

以此类推,当我们需要存储8时,需要再申请1字节的空间用来存储:

0 0 0 0 0 0 0 1
15 14 13 12 11 10 9 8

以此类推,BitMap使用位来存储数据相比较于其他数据结构可以大大节省存储空间。

2 Roaring BitMap原理

上面介绍的是基本的BitMap,在实际应用中,可能会面临两个问题:

  1. 数据碰撞:即将数据映射到BitMap时候会发生地址碰撞。

  2. 数据稀疏:即存入BitMap的数据比较稀疏,比如依次存入1,99999999,需要建立一个1000000000长度的BitMap,这个BitMap实际上只存入了2个数据,会浪费存储空间。

    其中1的问题可以用布隆过滤器 解决,2的问题则可以用RBM(Roaring BitMap)解决。

RBM原理可以归纳总结为如下三条

  1. 将 32-bit 的范围 ([0, n)) 划分为 2^16 个桶,即使用高16位来作为桶的编号。取高16位找到该条数据所对应的桶(可以理解为容器也可以理解为这个桶,容器和桶在这里可以理解为一个东西,只是说法不一样而已) ,这个桶有一个 Container 来存放该数值的低16位;

  2. 在存储和查询数值的时候,我们将一个数值 k 划分为高 16 位(k % 2^16)和低 16 位(k mod 2^16),取高 16 位找到对应的桶,然后在低 16 位存放在相应的 Container 中;

    比如我们要将31这个数放进roarigbitmap中,它的16进制为:0000001F,前16位为0000,后16为001F。所以我们先需要根据前16位的值:0,找到它对应的通的编号为0,然后根据后16位的值:31,确定这个值应该放到桶中的哪一个位置,如下图所示:

    png

  3. RBM 使用三种Container结构: Array Container 、 Bitmap Container和Run Container。Array Container 存放稀疏的数据,Bitmap Container 存放稠密的数据,Run Container存放连续的数据。下面分别介绍这三种Container结构:

    Array Container

    RBM在创建一个新的container时,如果只插入一个元素,默认会使用Array Container来进行存储,它内部的数据结构是一个 short array,这个 array 是有序的,方便查找。数组初始容量为 4,数组最大容量为 4096。超过最大容量 4096((16/8)B*4096/1024=8K) 时,会转换为 Bitmap Container。

    举例来说明数据放入一个 Array Container 的过程:有 0xFFFF0000 和 0xFFFF0001 两个数需要放到 Bitmap 中, 它们的前 16 位都是 FFFF,所以他们是同一个 key,它们的后 16 位存放在同一个 Container 中; 它们的后 16 位分别是 0 和 1, 在 Array Container 的数组中分别保存 0 和 1 就可以了,相较于原始的 Bitmap 需要占用 512M 内存(2^32/8/1024/1024)来存储这两个数,这种存放实际只占用了 2+4=6 个字节(key 占 2 Bytes,两个 value 占 4 Bytes,不考虑数组的初始容量)

    png

    如上图所示,4096这个阈值很聪明,低于它时ArrayContainer比较省空间,高于它时BitmapContainer比较省空间,可以最大限度地避免内存浪费。

    Bitmap Container:

    这个容器其实就是上一节的基础位图BitMap,只不过这里位图的位数为2^16(65536),也就是65536个bit,计算下来起所占内存就是8kb。然后每一位用0,1表示这个数不存在或者存在。当你存放的元素个数超过 4096 的时候,Array Container 的大小占用还是会线性的增长,但是 Bitmap Container 的内存空间并不会增长,始终还是占用 8 K,所以当 Array Container 超过最大容量(DEFAULT_MAX_SIZE)会转换为 Bitmap Container。

    比如我们要将987654321这个32bit的整数插入到RBM中,整个插入流程如下:

png

  1. 987654321转换为16进制为0x3ADE68B1,高16位为3ADE,低16位为68B1。
  2. 我们先用二分查找从一级索引(即 Container Array)中找到数值为 3ADE 的容器(如果该容器不存在,则新建一个),从图中我们可以看到,该容器是一个 Bitmap 容器。
  3. 找到了相应的容器后,看一下低 16 位的数值 68B1,10进制是 26801,因此在 Bitmap 中找到相应的位置,将其置为 1 即可。

Run Container:

这是一种利用步长来压缩空间的方法,我们举个例子:比如连续的整数序列 11, 12, 13, 14, 15, 27, 28, 29 会被 压缩为两个二元组 11, 4, 27, 2 表示:11后面紧跟着4个连续递增的值,27后面跟着2个连续递增的值,那么原先8 * 2B=16个字节的空间(short结构占用2B),现在只需要2 * 4=8个字节。最好情况是如果数据是连续分布的,就算是存放 65536 个元素,也只会占用 2 个 short。而最坏的情况就是当数据全部不连续的时候(比如填充所有的奇数或偶数位),会占用65536*2B/1024 = 128 KB 内存。

猜你喜欢

转载自juejin.im/post/7083861498384089101