基本思想:
不同于使用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亿个整数。
分析过程:
- 使用2bit,一个数字的状态只有三种,设定一个数字不存在为00,存在一次01,存在两次及其以上为10。
- 将20亿个数字放进去(存储),如果对应的状态位为00,则将其变为01,表示存在一次;如果对应的状态位为01,则将其变为10,表示已经有一个了,即出现多次;如果为10,则对应的状态位保持不变。
- 最后统计状态位为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应用示例
统计每日上线人数(活跃人数统计)
将日期作为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');
用来判断当天是否是第一次登录(或者每天限领一次的逻辑判断)
传统方式:在mysql中新建一张表,字段为(user_uuid, latest_login_time),用当前时间跟latest_login_time做对比,来判断是否是同一天登录。
使用Redis的BitMap的话,执行以下语句:$redis -> getBit("20210519", 123)
(其中123为用户uuid)同来做连续登陆奖励发放的条件判断
$redis -> bitop("AND", "stat_3_day_continue", "20210519", "20210518", "20210517");
参考文献
https://www.jianshu.com/p/608...
https://www.cnblogs.com/xuwc/...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。