基本思想
不同于使用int(long或double或...)类型来存储某个元素,BitMap采用一个bit位来标记某个元素(bit位的value为1表示该元素存在,此时该bit位的index即为该元素的值。)

算法

一个字节byte占用8个bit位,每个bit位的值为0或1,0代表该bit位没有元素存储,1代表该bit位的元素存在(元素的值为该bit位的index)。

通过这样的存储方式,假设原本10亿个unsigned int的数据量,
按传统占用的存储空间:10亿 * 4 / 2^30 ≈ 3.7G 现在只需要占用:10亿 / 8 / 2^30 ≈ 119M ,大大节省了存储空间。

给定一个元素n,如何确定存储在BitMap中哪个位置?
下面是BitMap基本存储操作(基于位运算)【JDK中BitMap的数据结构为BitSet】

/** 核心运算如下
index = n >> 3;
position = n & 7;

// add(n)
byte[index] |= 1 << position;

// clear(n)
byte[index] &= ~(1 << position);

// contain(n)
byte[index] & (1 << position) != 0 
**/
public class BitMap {
    //保存数据的
    private byte[] bits;
    
    //能够存储多少数据
    private int capacity;
    
    
    public BitMap(int capacity){
        this.capacity = capacity;
        
        //1bit能存储8个数据,那么capacity数据需要多少个bit呢,capacity/8+1,右移3位相当于除以8
        bits = new byte[(capacity >>3 )+1];
    }
    
    public void add(int num){
        // num/8得到byte[]的index
        int arrayIndex = num >> 3; 
        
        // num%8得到在byte[index]的位置
        int position = num & 0x07; 
        
        //将1左移position后,那个位置自然就是1,然后和以前的数据做|,这样,那个位置就替换成1了。
        bits[arrayIndex] |= 1 << position; 
    }
    
    public boolean contain(int num){
        // num/8得到byte[]的index
        int arrayIndex = num >> 3; 
        
        // num%8得到在byte[index]的位置
        int position = num & 0x07; 
        
        //将1左移position后,那个位置自然就是1,然后和以前的数据做&,判断是否为0即可
        return (bits[arrayIndex] & (1 << position)) !=0; 
    }
    
    public void clear(int num){
        // num/8得到byte[]的index
        int arrayIndex = num >> 3; 
        
        // num%8得到在byte[index]的位置
        int position = num & 0x07; 
        
        //将1左移position后,那个位置自然就是1,然后对取反,再与当前值做&,即可清除当前的位置了.
        bits[arrayIndex] &= ~(1 << position); 
    }
}

优点

  • 海量数据情况下,节省存储空间。
  • 运算效率高,不进行数值比较大小以及移动元素位置。

缺点

  • 不能对重复的数据进行排序(可以使用2bit查找重复的数据)
  • 当数据类似(1,1000,10万)只有3个数据的时候,用bitmap时间复杂度和空间复杂度相当大,只有当数据比较密集时才有优势。

应用

BitMap主要用于快速排序、查找、去重。

快速排序

时间复杂度:O(MAX/n),其中MAX表示待排序数组中最大的数字,n表示线程数。
空间复杂度:O(MAX/8)bytes

问题】假设我们要对5个不重复的数(4,7,2,5,3)排序。确定最大值是7,数值范围是0~7,共8个数,需要8bit,即1byte。

分析过程
将这些数对应的bit位置为1,遍历byte[],即可输出(2,3,4,5,7)。

快速去重

问题】20亿个整数中找出不重复的整数的个数,内存不足以容纳这20亿个整数。

分析过程

  1. 使用2bit,一个数字的状态只有三种,设定一个数字不存在为00,存在一次01,存在两次及其以上为10。
  2. 将20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;如果对应的状态位为01,则将其变为10,表示已经有一个了,即出现多次;如果为10,则对应的状态位保持不变。
  3. 最后统计状态位为01的个数。

问题】已知某个文件内包含一些电话号码,每个号码为8个数字,统计不同号码的个数。
解决方法: 8位最多99999999,占用100,000,000个bit,大小为12MB,这样用12M内存就表示来所有8位数的电话。

问题】一个序列里除了一个元素,其他元素都会重复出现3次,设计一个时间复杂度与空间复杂度最低的算法,找出这个不重复的元素。

快速查找

O(1)时间复杂度,见上述算法一节。

BitMap在Redis中的应用

命令

  • SETBIT(key offset value)

    对key所储存的字符串值,设置或清除指定偏移量上的位(bit)。
    其中offset为下标,value的值为0或1,时间复杂度O(1)。
  • GETBIT(KEY offset)

    对key所储存的字符串值,获取指定偏移量上的位(bit)。
    获得offset上的值,0或者1,复杂度为O(1)。
  • BITCOUNT(KEY)

    计算给定字符串中,被设置为1的比特位的数量。
    例如这个key保存的字符串二进制为101011,则返回结果4。
  • BITOP(operation destkey key[key...])

    对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
    operation 可以为AND、OR、NOT、XOR 这四种操作中的任意一种。

Redis Bitmap应用示例

  1. 统计每日上线人数(活跃人数统计)

    将日期作为key,将user_uuid作为offset,每当用户上线了,就执行以下语句:
    $redis -> setBit("20210519", 123, 1); (其中123为用户uuid)
    统计今天上线人数,执行以下语句:
    $redis -> bitCount("20210519");
    统计最近三天上线人数,执行以下语句:
    $redis -> bitop("OR", "last_3_days_count", "20210519", "20210518", "20210517");
    $result = $redis->bitCount('last_3_days_count');
  2. 用来判断当天是否是第一次登录(或者每天限领一次的逻辑判断)

    传统方式:在mysql中新建一张表,字段为(user_uuid, latest_login_time),用当前时间跟latest_login_time做对比,来判断是否是同一天登录。
    使用Redis的BitMap的话,执行以下语句:
    $redis -> getBit("20210519", 123) (其中123为用户uuid)
  3. 同来做连续登陆奖励发放的条件判断

    $redis -> bitop("AND", "stat_3_day_continue", "20210519", "20210518", "20210517");
参考文献
https://www.jianshu.com/p/608...
https://www.cnblogs.com/xuwc/...

th2009yu
1 声望0 粉丝

引用和评论

0 条评论