前言
很简单,最近看到了个以前没了解过过的数据结构RoaringBitmap
,所以想了解了解,顺便整理成一篇文章分享出来~
简介
RoaringBitmap
去掉Roaring
后就剩下bitmap
,那么很显然,RoaringBitmap
是bitmap
的升级版,或者说在某些方面更优~
例如: ConcurrentHashMap 和 HashMap
、MybatisPlus 和 Mybatis
等~ ,当然JavaScript 和 Java
除外
哈哈,回到文章,说到bitmap
,我们都知道其底层是bit
数组 ,因为在存储方面可以极大的节省空间,具体怎么节省呢~ 可以接着看看下面👇🏻
Java
中int
类型占4
个字节,1字节 = 8bit
那么如果此时我们要存储10
亿int
类型数字的话
- 正常我们需要
1000000000 * 4 / 1024 / 1024 ≈ 3814M
- 那么用
bitmap
来存储的话,则需要1000000000 / 8 / 1024 / 1024 ≈ 119M
这么一对比,bitmap
的优势就显而易见了,但有优点也会存在一定的缺点即不适合稀疏数据的存储 ,比如我要存储[1, 100, 1000, 10000000]的bit
数据,那么bitmap
申请的空间至少为10000000 / 8 / 1024 / 1024 ≈ 1.19M
,但实际我只存储了4
个数据,这样就造成了很大一部分的空间浪费~
故,RoaringBitmap
出世了!!! ,其解决了在存储大规模或稀疏数据时的一些缺点,并提供更好的压缩率和高性能的操作
RoaringBitmap实现原理
RoaringBitmap
将32
位的int
类型数据分为高16位
和低16位
,高16位
产生2^16
个桶,那么在存储和查找数据时,根据高16位
定位到对应的桶,再根据低16位
来确定放置的桶中的位置~
比如我们要存储: 2、65537、131072三个数字
- 数字
2
,高16位为0
,低16位为2
- 数字
65537
,高16位为1
,低16位为1
- 数字
131072
,高16位为2,
低16位为0`
上面的图乍一看像是HashMap
的数组 + 链表
结构,但其实并不是;
RoaringBitmap
中的"链表"
主要以下几种实现
- ArrayContainer
- BitmapContainer
- Runcontainer
ArrayContainer
听这名字就知道底层是采用的数组实现的 ,ArrayContainer
会将低16位
直接保存到数组中,同时保持有序,方面二分查找,如果存储空间不够会现行扩容在进行存储;
这样的存储方式并没有进行压缩,因为不适合存储大量数据,最大存储数量默认为4096
,超过这个阈值即会将ArrayContainer
转为BitmapContainer
结合图示来看
-
当存储个数小于
4096
时 ,BitmapContainer
的内存占用比ArrayContainer
低 -
当存储个数大于
4096
时 ,ArrayContainer
的内存占用比BitmapContainer
低
BitmapContainer
底层采用bitmap
实现,采用long[]
存储数据,long
占8
个字节即64bit
,而我们存储的是低16
位数据,最大为65536
,65536 / 64 = 1024
,所以需要长度为1024
的long[]
所以无论存储1个还是1024个元素,占用空间都是8byte * 1024 = 8kb
Runcontainer
RunContainer
采用行程长度压缩算法(Run Length Encoding) ,对于连续数据有比较好的压缩效果。
原理是: 对于连续出现的数字,只记录初始数字和后续数量。如下👇🏻
- 对于数列
11
,它会压缩为11,0
; - 对于数列
11,12,13,14,15
,它会压缩为11,4
; - 对于数列
11,12,13,14,15,21,22
,它会压缩为11,4,21,1
;
低版本 源码中采用short[] valueslength
存就是压缩后的数据,高版本中改成了char[] valueslength
那么对于RunContainer
来说,最差的情况就是存储0 ~ 65525
的所有奇数或者所有偶数 ,这样一来就不会进行压缩 ,即最大占用空间为(65536 / 2) * 2byte / 1024 ≈ 64M
RoaringBitmap使用
先导入maven
依赖
xml
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>0.9.44</version>
</dependency>
下面简单列了下常用的api,更多的话可以去网上搜搜啦~
java
public class RoaringBitMapTest {
public static void main(String[] args) {
RoaringBitmap bitmap_1 = new RoaringBitmap();
for (int i = 0; i < 10; i++) {
// 添加元素
bitmap_1.add(i);
}
// 打印元素
System.out.println(Arrays.toString(bitmap_1.stream().toArray()));
// 判断元素是否存在
System.out.println(bitmap_1.contains(0));
System.out.println(bitmap_1.contains(100));
RoaringBitmap bitmap_2 = new RoaringBitmap();
for (int i = 5; i < 20; i++) {
// 添加元素
bitmap_2.add(i);
}
// 合并bitmap_1、bitmap_2,取交集
RoaringBitmap andBitmap = RoaringBitmap.and(bitmap_1, bitmap_2);
System.out.println(Arrays.toString(andBitmap.stream().toArray()));
// 合并bitmap_1、bitmap_2,取并集并去重
RoaringBitmap orBitmap = RoaringBitmap.or(bitmap_1, bitmap_2);
System.out.println(Arrays.toString(orBitmap.stream().toArray()));
// 取bitmap_1有但bitmap_2没有的数据
RoaringBitmap integers = RoaringBitmap.andNot(bitmap_1, bitmap_2);
System.out.println(Arrays.toString(integers.stream().toArray()));
}
}
RoaringBitmap源码
构造函数
java
public class RoaringBitmap implements Cloneable, Serializable, Iterable<Integer>, Externalizable,
ImmutableBitmapDataProvider, BitmapDataProvider, AppendableStorage<Container> {
RoaringArray highLowContainer = null;
public RoaringBitmap() {
// 初始化桶
highLowContainer = new RoaringArray();
}
}
public final class RoaringArray implements Cloneable, Externalizable, AppendableStorage<Container> {
static final int INITIAL_CAPACITY = 4;
char[] keys = null;
Container[] values = null;
int size = 0;
protected RoaringArray() {
// 默认容量为4
this(INITIAL_CAPACITY);
}
RoaringArray(int initialCapacity) {
this(new char[initialCapacity], new Container[initialCapacity], 0);
}
RoaringArray(char[] keys, Container[] values, int size) {
// keys就是前面提到的高16位桶
this.keys = keys;
// values就是存储低16位的桶
this.values = values;
this.size = size;
}
}
如上源码所示,RoaringBitmap
构造函数中初始化了高16位桶
和低16位桶
add
java
public void add(final int x) {
// 拿到高16位
final char hb = Util.highbits(x);
// 根据高16位找到对应的桶index
final int i = highLowContainer.getIndex(hb);
if (i >= 0) {
// 桶已存在,则根据低16位存储到桶内
highLowContainer.setContainerAtIndex(i,
highLowContainer.getContainerAtIndex(i).add(Util.lowbits(x)));
} else {
// 桶不存在,则创建并设置好桶,存放低16位数据
final ArrayContainer newac = new ArrayContainer();
highLowContainer.insertNewKeyValueAt(-i - 1, hb, newac.add(Util.lowbits(x)));
}
}
步骤
- 拿到存储数据的高16位
- 根据高16位 找到对应的桶
index
- 桶已存在,则根据低16位存储到桶内
- 桶不存在,则创建并设置好桶,存放低16位数据
contains
java
public boolean contains(final int x) {
// 获取高16位
final char hb = Util.highbits(x);
// 根据高16位拿到低16位对应的桶
final Container c = highLowContainer.getContainer(hb);
// 判断低16位是否存在
return c != null && c.contains(Util.lowbits(x));
}
protected Container getContainer(char x) {
// 根据高16位拿到桶对应的index
int i = getContainerIndex(x);
if (i < 0) {
return null;
}
// 返回对应的桶
return this.values[i];
}
// ArrayContainer
public boolean contains(final char x) {
// 通过二分查找查询低16位index,如果大于等于0,说明存在
return Util.unsignedBinarySearch(content, 0, cardinality, x) >= 0;
}
步骤
- 获取高16位
- 根据高16位获取低16位对应的桶
- 以
ArrayContainer
为例,其contains
内部通过二分搜索查询低16位
的index
,如果大于等于0
则说明存在
桶扩容
在上面的add
方法中,如果桶不存在则会调用insertNewKeyValueAt
进行创建并存储低16位
数据
java
void insertNewKeyValueAt(int i, char key, Container value) {
// 检查是否需要扩容
extendArray(1);
// 存放桶并插入数据
System.arraycopy(keys, i, keys, i + 1, size - i);
keys[i] = key;
System.arraycopy(values, i, values, i + 1, size - i);
values[i] = value;
size++;
}
java
void extendArray(int k) {
// 如果桶数量 + k 大于最大容量,则需要进行扩容
if (this.size + k > this.keys.length) {
int newCapacity;
if (this.keys.length < 1024) {
newCapacity = 2 * (this.size + k);
} else {
newCapacity = 5 * (this.size + k) / 4;
}
this.keys = Arrays.copyOf(this.keys, newCapacity);
this.values = Arrays.copyOf(this.values, newCapacity);
}
}
如果当前桶数量 + 要添加的桶数量 大于最大容量,则需要进行扩容
- 当前桶容量小于
1024
时 ,新容量为2 * (this.size + k)
- 当前桶容量大于
1024
时 ,新容量为5 * (this.size + k) / 4
扩展
上面说到的RoaringBitmap
存储的都是int
类型的数据,但是在实际业务中我们可能还需要存储long
型的数据,这时候RoaringBitmap
就不太适用了,我们需要改用Roaring64NavigableMap
。
api
基本与RoaringBitmap
一致,只是底层逻辑上会有变动,留到下次再进行探究~
java
public class RoaringBitMapTest {
public static void main(String[] args) {
Roaring64NavigableMap roaringMap = new Roaring64NavigableMap();
for (long i = 0; i < 10; i++) {
roaringMap.addLong(i);
}
System.out.println(Arrays.toString(roaringMap.stream().toArray()));
System.out.println(roaringMap.contains(0));
System.out.println(roaringMap.contains(100));
}
}
总结
本文从RoaringBitmap
理论出发,横贯原理、源码,真可谓一条龙服务,求个赞不过分吧(狗头)