Bitmap魔法揭秘:助力高效数据存储与统计

一句话介绍bitmap

只需消耗极小的存储空间,即可高效的满足大数据规模下的查询、统计、去重等使用场景。

bitmap的基本思想

我们假设数据只有两种状态,那么刚好就可以用1个bit位的0,1来分别表示,这样计算下来即使有1亿条这样的数据,那也只需要用到(100000000 / 8 / 1024 / 1024 ≈ 11.92MB),大约12M的空间。同样的条数,如果换到Java中常用的int数据类型来存储,则需要用到(100000000 * 4 / 1024 / 1024 ≈ 381.46MB),大约381M的空间,数据越多,32倍的差异就越明显。

真的有那么多只用两种状态就能描述的数据吗?

只有两种状态的数据的应用场景看起来比较局限,但其实已经能解决非常多的类似于是与非的日常问题了。

除此之外,虽然我们非常清楚客观世界并非是非黑即白的,但我们要想得到一种结果往往又会进行一定程度上的简化和分类,以此来支持我们的行动和决策。比如:统计用户日活,我们简单的归为当日登陆和当日未登陆。把复杂问题经过简化后的这类场景,看起来就又回到了是与非的问题了。

不适合bitmap的场景

  1. 如果数据状态比较多,无法用1个bit位来表示,自然就不适合使用bitmap。
  2. 数据不可重复既是优点也是缺点,优点在于不可重复性可以用来判断是否存在,从而满足某些业务场景。同样,不可重复则表示如果添加了一条已经存在的数据,则会忽略本次操作。
  3. 如果数据比较稀疏则可能会导致bitmap占用较大空间,因为每一条数据都会映射在bitmap中的某一个位点上,假设有两条数据分别是0,99999,此时对于bitmap来说虽然只有两条数据,但依旧要申请99999个位的空间大小,才能满足这两条数据的存放。

基于bit的基本操作

在应用bitmap之前,有必要了解一下关于位运算的一些常见操作,见识一下只有0,1的数据是如何释放出巨大的运算能量的。

and运算

奇偶判断

x and 1可以用来取x二进制的最末位,常用来判断一个数的奇偶性,x and 1结果为0,则x为偶数,结果为1,则x为奇数。

java 复制代码
if (x & 1 == 0) {
    "偶数"
} else {
    "奇数"
}

清零操作

x and (x + 1),把右边连续的1变成0。

x and (x - 1),把最后一个1变为0。

x and 0,所有位都改为0。

取末位

和奇偶判断一样,x and 1可以用来取x二进制的最末位,那么x and 11同样可以用来取x二进制的最后2位,x and 111可以用来取x二进制的最后3位,x and 1111可以用来取x二进制的最后4位,以此类推。

判断指定位是否为1

java 复制代码
public static boolean 判断指定位是否为1(int num, int index){
    return ((1 << index) & num) != 0;
}

or运算

把最后一位变1

x or 1,将二进制的最后一位改为1。

从右数,把第一个0变为1

x or (x + 1)

从右数,把连续的0变为1

x or (x - 1)

左移和右移

a << b 就表示在二进制a后面添加b个0,比如8 << 2等于32,因为十进制8,二进制表示为1000,后面添加2个0,则变成:100000,转换成十进制就是32

a >> b则表示去除二进制a后面b位,同理,8 >> 2等于2

修改操作

将指定位设置为1

举个例子,二进制数为1010,现在需要将第2位(下标从0开始,从右向左数)设置为1。

首先通过左移操作:1 << 2,构建一个二进制数:0100,然后将这个二进制数与原二进制进行按位或计算,即可实现将原二进制数第2位设置为1。

java 复制代码
public int 将指定位置设置为1(int num, int index) {
    return (1 << index) | num;
}

将指定位设置为0

还是1010这个数,现在需要将第1位(下标从0开始,从右向左数)设置为0。

首先通过左移操作:1 << 1,构建一个二进制数:0010,然后取反得到:1101,最后将这个二进制数与原二进制进行按位与计算,即可实现将原二进制数第1位设置为0。

java 复制代码
public int 将指定位置设置为0(int num, int index) {
    return ~(1 << index) & num;
}

统计操作

统计二进制中有多少个1

利用x and (x - 1),可以把最后一个1变为0的性质,不断循环判断,如果满足x > 0,则记录数加1,,且继续执行x and (x - 1)计算,直到不满足条件为止。

java 复制代码
public int cnt1(int num){
    int cnt = 0;
    while(num > 0){
        num &= num - 1;
        cnt++;
    }
    return cnt;
}

统计二进制中最长连续1的长度

java 复制代码
public int len1(int num){
    int len = 0;
    int cnt = 0;
    while (num > 0) {
        if ((num & 1) == 1) {
            cnt++;
        } else {
            cnt = 0;
        }
        len = Math.max(len, cnt);
        num = num >> 1;
    }
    return len;
}

统计某一段区间内1的个数

java 复制代码
public int count_one_in_range(int num, int start, int end){
    int len = end - start + 1;
    int mask = ((1 << len) - 1) << start;
    int x = mask & num;
    return cnt1(x);
}

public int cnt1(int num){
    int cnt = 0;
    while(num > 0){
        num &= num - 1;
        cnt++;
    }
    return cnt;
}

业务场景

可以看到,利用多个不同维度bitmap的位运算可以适用于非常多的统计类业务场景,如:

  1. 求bitmap中1的总个数就是日活。
  2. 求最长连续的1就是最长连续访问天数。
  3. 求新增用户就是(bitmap1 | bitmap2) ^ bitmap1
  4. 求某3天内访问过的用户bitmap1 | bitmap2 | bitmap3,相当于求并集。
  5. 求某3天内每天都访问过的用户bitmap1 & bitmap2 & bitmap3,相当于求交集。

实际应用案例

Redis Bitmap:实现千万级用户签到的秘密武器,这篇文章中详细讲述了如何通过redis提供的bitmap这种数据结构实现签到业务。

相关推荐
WaaTong1 分钟前
Java反射
java·开发语言·反射
理想不理想v8 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
齐 飞25 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
九圣残炎34 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge36 分钟前
Netty篇(入门编程)
java·linux·服务器
LunarCod42 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
Re.不晚1 小时前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐1 小时前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
sszmvb12341 小时前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端