万书网 > 文学作品 > 漫画算法:小灰的算法之旅 > 6.2Bitmap的巧用

6.2Bitmap的巧用





6.2.1 一个关于用户标签的需求

为了帮助公司精准定位用户群体,咱们需要开发一个用户画像系统,实现用户信息的标签化。

用户标签包括用户的社会属性、生活习惯、消费行为等信息,例如下面这个样子。

小灰的用户标签

通过用户标签,我们可以对多样的用户群体进行统计。例如统计用户的男女比例、统计喜欢旅游的用户数量等。

放心吧,这个需求交给我一定会妥妥的!

为了满足用户标签的统计需求,小灰利用关系型数据库设计了如下的表结构,每一个维度的标签对应着数据库表中的一列。

要想统计所有“90后”的程序员,该怎么做呢?

用一条求交集的SQL语句即可。

Select  count(distinct  Name)  as  用户数  from  table  where  age  =  '90  后'  and  Occupation  =  '  程序员'  ;

要想统计所有使用苹果手机或“00后”的用户总和,该怎么做呢?

用一条求并集的SQL语句即可。

Select  count  (distinct  Name)  as  用户数  from  table  where  Phone  =  '苹果'  or  age  =  '00  后'  ;

看起来很简单嘛,嘿嘿……

两个月之后……

事情没那么简单,现在标签越来越多,例如用户去过的城市、消费水平、爱吃的东西、喜欢的音乐……都快有上千个标签了,这要给数据库表增加多少列啊!

筛选的标签条件过多的时候,拼出来的SQL语句像面条一样长……

不仅如此,当对多个用户群体求并集时,需要用distinct来去掉重复数据,性能实在太差了……



6.2.2 用算法解决问题

小灰,你怎么愁眉苦脸的呀?

唉,还不是被一个需求折腾的!

事情是这样子的……(小灰把工作中的难题告诉了大黄)

哈哈,小灰,你听说过Bitmap  算法吗?在中文里又叫作位图算法。

我又不是搞计算机图形学的,研究位图算法干什么?

这里所说的位图并不是像素图片的位图,而是内存中连续的二进制位(bit)所组成的数据结构,该算法主要用于对大量整数做去重和查询操作。

举个例子,假设给出一块长度为10bit的内存空间,也就是Bitmap,想要依次插入整数4、2、1、3,需要怎么做呢?

很简单,具体做法如下。

第1步,给出一块长度为10的Bitmap,其中的每一个bit位分别对应着从0到9的整型数。此时,Bitmap的所有位都是0(用紫色表示)。

第2步,把整型数4存入Bitmap,对应存储的位置就是下标为4的位置,将此bit设置为1(用黄色表示)。

第3步,把整型数2存入Bitmap,对应存储的位置就是下标为2的位置,将此bit设置为1。

第4步,把整型数1存入Bitmap,对应存储的位置就是下标为1的位置,将此bit设置为1。

第5步,把整型数3存入Bitmap,对应存储的位置就是下标为3的位置,将此bit设置为1。

如果问此时Bitmap里存储了哪些元素。显然是4、3、2、1,一目了然。

Bitmap不仅方便查询,还可以去掉重复的整数。

看起来有点意思,可是Bitmap算法跟我的项目有什么关系呢?

你仔细想一想,你所做的用户标签能不能用Bitmap的形式进行存储呢?

我的每一条用户数据都对应着成百上千个标签,怎么也无法转换成Bitmap的形式啊?

别急,我们不妨把思路逆转一下,为什么一定要让一个用户对应多个标签,而不是一个标签对应多个用户呢?

一个标签对应多个用户?让我想想啊……

我明白了!信息不一定非要以用户为中心,也能够以标签为中心来存储,让每一个标签存储包含此标签的所有用户ID,就像倒排索引一样!

第1步,建立用户名和用户ID的映射。

第2步,让每一个标签存储包含此标签的所有用户ID,每一个标签都是一个独立的Bitmap。

这样一来,每一个用户特征都变得一目了然。

例如程序员和“00后”这两个群体,各自的Bitmap分别如下。

Bingo!这就是Bitmap算法的运用。

我还有一点不太明白,使用哈希表也同样能实现用户的去重和统计操作,为什么一定要使用Bitmap呢?

傻孩子,如果使用哈希表的话,每一个用户ID都要存成int或long类型,少则占用4字节(32bit),多则占用8字节(64bit)。而一个用户ID在Bitmap中只占1bit,内存是使用哈希表所占用内存的1/32,甚至更少!

不仅如此,Bitmap在对用户群做交集和并集运算时也有极大的便利。我们来看看下面的例子。

1.  如何查找使用苹果手机的程序员用户

2.  如何查找所有男性用户或“00后”用户

这就是Bitmap算法的另一个优势——高性能的位运算。

原来如此。我还有一个问题,如何利用Bitmap实现反向匹配呢?例如我想查找非“90后”的用户  ,如果简单地做取反运算操作,会出现问题吧?

会出现什么问题呢?我们来看一看。

“90后”用户的Bitmap如下。

如果想得到非“90后”  的用户,能够直接进行非运算吗?

显然,非“90后”用户实际上只有1个,而不是图中所得到的8个结果,所以不能直接进行非运算。

这个问题提得很好,但是也不难解决,我们可以借助一个全量的Bitmap。

同样是刚才的例子,我们给出“90后”用户的Bitmap,再给出一个全量用户的Bitmap。最终要求出的是存在于全量用户,但又不存在于“90后”用户的部分。

如何求出这部分用户呢?我们可以使用异或  运算进行操作,即相同位为0,不同位为1。

我明白了,这真是个好方法!那么Bitmap的代码该怎么来实现呢?

Bitmap的实现方法稍微有些难理解,让我们来看看代码。

1.  //  每一个word是一个long类型元素,对应一个64位二进制数据

2.  private  long[]  words;

3.  //Bitmap的位数大小

4.  private  int  size;

5.

6.  public  MyBitmap(int  size)  {

7.  this.size  =  size;

8.  this.words  =  new  long[(getWordIndex(size-1)  +  1)];

9.  }

10.

11.  /**

12.  *  判断Bitmap某一位的状态

13.  *  @param  bitIndex  位图的第bitIndex位

14.  */

15.  public  boolean  getBit(int  bitIndex)  {

16.  if(bitIndex<0  ||  bitIndex>size-1){

17.  throw  new  IndexOutOfBoundsException("  超过Bitmap有效范围");

18.  }

19.  int  wordIndex  =  getWordIndex(bitIndex);

20.  return  (words[wordIndex]  &  (1L  <<  bitIndex))  !=  0;

21.  }

22.

23.  /**

24.  *  把Bitmap某一位设置为true

25.  *  @param  bitIndex  位图的第bitIndex位

26.  */

27.  public  void  setBit(int  bitIndex)  {

28.  if(bitIndex<0  ||  bitIndex>size-1){

29.  throw  new  IndexOutOfBoundsException("  超过Bitmap有效范围");

30.  }

31.  int  wordIndex  =  getWordIndex(bitIndex);

32.  words[wordIndex]  |=  (1L  <<  bitIndex);

33.  }

34.

35.  /**

36.  *  定位Bitmap某一位所对应的word

37.  *  @param  bitIndex  位图的第bitIndex位

38.  */

39.  private  int  getWordIndex(int  bitIndex)  {

40.  //右移6位,相当于除以64

41.  return  bitIndex  >>  6;

42.  }

43.

44.  public  static  void  main(String[]  args)  {

45.  MyBitmap  bitMap  =  new  MyBitmap(128);

46.  bitMap.setBit(126);

47.  bitMap.setBit(75);

48.  System.out.println(bitMap.getBit(126));

49.  System.out.println  (bitMap.getBit(78));

50.  }

在上述代码中,使用一个命名为words的long类型数组来存储所有的二进制位。每一个long元素占用其中的64位。

如果要把Bitmap的某一位设为1,需要经过两步。

1.  定位到words中的对应的long元素。

2.  通过与运算修改long元素的值。

如果要查看Bitmap的某一位是否为1,也需要经过两步。

1.  定位到words中的对应的long元素。

2.  判断long元素的对应的二进制位是否为1。

有了Bitmap的基本读写操作,该如何实现两个Bitmap的与、或、异或运算呢?感兴趣的读者可以思考一下。

想要深入研究Bitmap算法的读者,可以看一下JDK中BitSet类的源码。同时,缓存数据库Redis中也有对Bitmap算法的支持。

虽然有现成的工具类和数据库,但我们仍然应该了解Bitmap算法的底层原理和实现方式。

今天就介绍到这里,咱们下一节再见!