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这种数据结构实现签到业务。

相关推荐
garmin Chen7 分钟前
从 Transformer 到 Agent:大模型技术全景解析
java·人工智能·python·深度学习·transformer
愚公移码12 分钟前
蓝凌EKP18产品:流程引擎技术篇之流程核心概念模型
java·人工智能·流程引擎·蓝凌
hzhsec14 分钟前
启明星辰(安全服务实习生)面试题
网络安全·面试
Full Stack Developme19 分钟前
Apache Tika 教程
java·开发语言·python·apache
鹅城剑仙23 分钟前
Spring Boot 微服务架构设计与最佳实践
spring boot·后端·微服务
鹅城剑仙33 分钟前
Java线程池完全指南
java
李白的天不白35 分钟前
SmartAdmin(基于 Spring Boot 框架)中配置跨域请求 VUE3 设置请求头
java·前端
橙子进阶之路37 分钟前
Java线程(CompletableFuture)
java·开发语言
鹅城剑仙1 小时前
Java CompletableFuture 异步编程完全指南
java
2601_961875241 小时前
法考备考计划表|学习计划|资料已整理
java·开发语言·学习·eclipse·tomcat·c#·hibernate