位图算法BitMap

内容来自:
https://zhuanlan.zhihu.com/p/379713260


概念

BitMap的核心思想:用一个bit位来记录0和1两种状态,将具体数据映射到比特数组的具体某一位上,这个bit位设置为0表示该数不存在,设置为1表示该数存在。
BitMap的用途:可以用来处理大量数据的排序、查询以及去重等,BitMap在用户群做交集和并集运算的时候也有极大的便利。
在数据量越大的情况下,BitMap节省空间的效果就越显著。所以BitMap很适合用来进行大量数据的排序、去重、查找,包括在线活跃用户的统计,用户签到等。

10进制到2进制映射。
可以用int(32bit)或者long(64bit)来进行映射。如果使用int进行映射,假设要排序的数有N个,那么需要申请的内存空间大小就是int[(N - 1) / 32 + 1],映射关系如下:
a[0]:0 ~ 31
a[1]:32 ~ 63
a[2]:64 ~ 95

可知,给定一个数num在对应数组a[i]中的下标为 num % 32。

一个数num存放在a[num / 32]的num % 32下标上。可以通过移位操作将对应位置1
a[n >> 5] |= 1 << (n & 0x1F)
n & 0x1F 保留n的后五位,相当于 n % 32。

代码

用java实现一个简单的BitMap:

import java.util.Arrays;

public class BitSet {
    private int[] bits;

    private final static int ADDRESS_BITS_PER_WORD = 5;
    private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;

    /**
     * 无参构造器
     * 默认构造容量为32bit的数组,即数组长度为1
     */
    public BitSet() {
        bits = new int[(BITS_PER_WORD - 1) >> ADDRESS_BITS_PER_WORD + 1];
    }

    /**
     * 有参构造器
     * @param nbits 数字的个数
     */
    public BitSet(int nbits) {
        bits = new int[(nbits - 1) >> ADDRESS_BITS_PER_WORD + 1];
    }

    public int[] getBits() {
        return bits;
    }

    /**
     * 把num映射到bits数组中
     * @param num
     */
    public void set(int num) {
        // num在数组中的下标
        int index = num >> ADDRESS_BITS_PER_WORD;
        // TODO:要检查数组是否需要扩容
        bits[index] |= 1 << (num & 0x1F);
    }

    /**
     * 判断bits数组中对应位的值
     * @param bitIndex
     * @return
     */
    public boolean get(int bitIndex) throws Exception {
        if (bitIndex < 0) {
            throw new Exception();
        }
        // 把输入的下标进行转换,对应数组某个值的某个位置
        int index = bitIndex >> ADDRESS_BITS_PER_WORD;
        return (bitIndex < bits.length) && ((bits[index] & (1 << (bitIndex & 0x1F))) != 0);
    }

    public static void main(String[] args) throws Exception {
        // 5亿个数
        BitSet bitSet = new BitSet(1_0000_0000);
        // 目标数组
        int[] arr = {2, 98, 76, 56, 100, 762, 16, 95};
        Arrays.stream(arr).forEach(num -> {
            bitSet.set(num);
        });

        // 判断某个数在数组中是否存在
        System.out.println(bitSet.get(100));  // true
        System.out.println(bitSet.get(200));  // false
        System.out.println(bitSet.get(762));  // true

        // 输出排序后的数组
        int[] res = bitSet.getBits();
        int count = 0;
        for (int i = 0; i < res.length; i++) {
            // 按位输出
            for (int j = 0; j < 32; j++) {
                // 为1表示该数存在
                if (((res[i] >> j) & 1) == 1) {
                    arr[count++] = i * 32 + j;
                }
            }
        }
        // 2 16 56 76 95 98 100 762
        Arrays.stream(arr).forEach(System.out::println);
    }
}
bits[0][0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
bits[1][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
bits[2][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
bits[3][0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
bits[4][0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ......

一些位图算法对应的开源实现:
JDK的BitSet和Google的EWAHCompressedBitmap,Redis里也提供了类似的一些命令,主要有以下几个:SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS,BITFIELD。

优缺点

优点:
(1)运算效率高。
(2)占用内存少。

缺点:
(1)对重复数据无法进行排序。
(2)数据碰撞。比如将字符串映射到 BitMap 的时候会有碰撞的问题,那就可以考虑用 Bloom Filter 来解决,Bloom Filter 使用多个 Hash 函数来减少冲突的概率。
(3)数据稀疏时浪费空间。可以通过引入 Roaring BitMap 来解决。

发表评论