Bit-Map算法
简介:
1:Bit-Map算法又名位图算法,其原理是,使用下标代替数值或特定的意义,使用这个位为0或者1代表特性是否存在。
2:Bit-Map算法具有效率高,节省空间的特点,适用于对大量数据进行去重,查询等。
应用举例:
例如,我们存储了一些整形数据:2,8,4,6,9,我们需要查询是否存储了3,那么,按普通的思路,我们需要将所有数据遍历一次,看是否有需要查找的数据,这样,时间复杂度为O(n),使用Bit-Map,我们可以申请10 bit 的空间,并将内容全部置零,需要存储2,那么就将下标2的bit位置为1,存储8,就将下标为8的bit位置为1,同样的,将下标为4,6,9的bit位也置为1,其结果如下图所示:
图中,蓝色代表0,红色代表1。
此时,如果我们需要查找3是否保存过,只需要直接访问下标3的bit位,为1,就说明保存过,为0就是未保存过。
当然,这只是Bit-Map算法的最简单举例,主要是为了理解Bit-Map算法的实现方式。
Bit-Map算法实际运用:
在实际中,例如某位QQ好友的信息页面中,我们常看见各种标签:宅男、90后、吃货、手机控……。再例如一些管理软件,通常会保存一些进度信息,比如物流订单状态:下订单、付款、发货……。
使用数据库:
为了存储这些数据,我们很容易就想到了数据库,将腾讯所有存在的QQ号都保存在数据库中,添加很多的标签,记录每个QQ号有的标签,当我们查询的时候,就找到这个QQ号,就能查看到里面的所有标签,但是,当我们要访问哪些QQ是90后,哪些QQ是宅男,也是吃货,是手机控,是……,的时候,我们需要写很多SQL求交集等命令,导致命令非常长,难写而且效率低。当我们的标签上千上万后,就会非常地复杂。
使用Bit-Map算法:
我们可以发现,这些信息都是是或者不是(是90后,不是90后)那么,这些数据都可以使用Bit-Map高效的存储起来,也可以非常方便地查询某一条记录的状态。
首先,我们将所有QQ号按一定规则排序,每个QQ号都有自己对应的下标,如下图:
我们让每一个标签都有独立的Bit-Map图,标签一般情况下是按照顺序排列的,于是,对于90后这个标签,我们可以建立一个BitMap,如下:
其中,红色代表1,即是90后,蓝色代表0,即非90后,ID号及每个对象的ID,上图就可以代表ID为0~9的QQ号的是否为90后的状态,仅仅占10bit空间。同样,对于是否为吃货,是否使用的小米手机,是否是程序员等等,都是可以射阳表示的,每个标签对应一个BitMap图,ID个数(实际上是ID号的最大数值)决定了每个BitMap的大小。如果ID是不连续的,例如ID只有0和10000两个对象,那么按上面这种,每个BitMap就会占用10001个bit,对于这种特殊情况,后面在做讨论研究。
BitMap算法的数据获取:
1:在对每个标签建BitMap图之后,他要获取即是90后,又是程序员的对象,就可以直接使用这两个标签的BitMap图相与,结果为一的ID,对应的对象就是要找的,当有多个条件的时候,同样只需要将多个标签相与即可得到结果,这里就不举例说明了,无论是空间还是时间上分析,效率都非常高。
2:BitMap算法同样支持或运算,例如,我们要查找是90后或者00后的对象,同样也只需要将这两个标签的BitMap图相或即可得到结果。
BitMap算法不支持非运算,在不使用其他方法处理的时候,我们不可以得到非90后的对象,如果直接对整个BitMap图做非运算,那么得到的是90后对象以外的所有,但这个所有并不是每一个都是对象,例如:总共有三个对象,ID为0和为1的对象为90后,ID为2的为00后,然而我们使用了10bit的空间存储,显然,3~9bit是没有对应对象的,直接做非运算,得到的结果显然是ID为3~9的也是非90后,显然是错的。
非运算解决方案:我们可以做一个全量用户的BitMap,这个BitMap里面为1代表这个ID对应有对象,否则就是没有对象,这样,我们要得到非90后可以先做非运算,得到90后ID之外其他ID,再与全量用户做与运算,即可得到这些“非90后”里面,有对应对象的部分,这些就是实实际际的非90后对象。
BitMap算法缺点:
1:BitMap不直接支持非运算,通过上述的方法可以简单解决,但需要对建立一个全量的BitMap图,这不算很致命的缺点,可以较容易的解决。
2:添加对象不方便:每次我们需要添加对象的时候,就需要改变每一个BitMap图的长度,需要做大量的数据复制,空间分配运算,当然,我们可以像STL算法里面那样,每次分配两倍的空间(或者根据情况适当多分配一些空间),这样,添加少量的对象就不用做大量内存操作和文件操作,只需要将全量BitMap里面添加的ID设置成1,其他的标签也一样,该怎么变就怎么变。
3:在之间插入ID,或者删除中间ID(例如,某个QQ号被回收,不再使用,很久以后又被使用),会导致空间浪费,如果对对象对应的ID顺序没有要求,可以直接查找前面空闲的空间,如果有要求,那么久只能做大量的数据操作。通常情况下,是没有要求的,而且一般一开始对象ID都会是按照顺序的,这样就不需要考虑这个问题。
4:当我们分配的ID不连续的时候,会导致空间浪费,就像之前的,只有0和10000两个对象,每个标签就需要10001个bit来保存,非常浪费空间,这也是BitMap的最大缺点之一,谷歌在自己实现的EWAHCompressedBitMap中,就对BitMap存储空间做了一定的优化:
BitMap算法空间优化:
EWAH把BitMap存储在long(64位)数组中,long数组的每一个元素都可以当作是一个64位的二进制,他是EWAH BitMap里面最小的单位,我们称一个long为一个Word,如下:
现在我们插入一个ID是1的用户:Word1的内容就变成00000010B,再插入ID为4的用户,Word1的内容就变成了00010010B,ID为多少,对应的位就变为1,由此可见,一个Word最多可以保存64个ID,如果要保存ID为64的用户,那么Word2就变成00000001B,要保存ID为129的用户,Word3就变成了00000010B。但是,为什么是从Word1开始而非Word0呢?因为Word0适用于记录位置信息的。
Word分为两种,一种直接存储数据,称为Literal Word(LW),另一种保存跨度信息,称为Running Length Word(RLW)。我们已知LW的用处是直接存储的数据,下面对RLW存储的跨度信息做说明:
RLW分为高32位和低32位,其中高32位用于保存当前RLW后面连续接了多少个LW,即表示后面跟了多少个Word的数据,低32位表示当前Word横跨了多少个空Word。例如:
其中,Word0代表了后面有3个连续的LW(数据Word),自身横跨0个Word。Word4代表自身横跨6247个Word,后面连续存储了1个Word,我们知道每个Word最多存64个ID,因此,我们可知Word0后面的LW最大ID是:
64*3-1 = 191
Word4说明了后面紧跟的为一个Word,因此,Word5能存储的最小ID为:
191+6247*64 = 399999
Word5能存储的范围为399999~400063。上图中Word5存储的ID就是399999。
对于其他标签的数据,同样是这样存储,不需要建立很长的BitMap来存储。
在之间插入:
如果在上述优化的算法中,在之间插入ID为200000的ID,那么就会导致后面RLW的数据变化。但相比最原始的BitMap插入还是会快。但,任然不建议在之间插入,最好是按顺序插入。