RoaringBitmap
是一种高效的位图数据结构,它通过压缩技术有效地存储和操作大量整数数据,特别适合整数集合的集合运算(如并集、交集和差集)。其中,RoaringArray
是 RoaringBitmap
的核心部分之一,它管理多个容器,用于存储整数的分片数据。RoaringArray
使用了分片机制,将整数集合划分为多个“块”(block),并为每个块配备一个 key
和一个 value
。
在 RoaringBitmap 中,RoaringArray
的 key
和 value
的底层原理如下:
1. RoaringBitmap 的基本结构和分片机制
RoaringBitmap 的设计基于分片(partitioning)的思想。整个整数范围被分成固定大小的块,每个块包含 2^16(即 65536)个连续的整数。一个整数 x
被分解为两个部分:
- 高 16 位作为块的
key
。 - 低 16 位作为块内的偏移量,用于找到具体的
value
。
例如,假设我们有一个整数 x
,它的二进制表示为 xxxxxxxxxxxxxxxx_yyyyyyyyyyyyyyyy
,其中:
xxxxxxxxxxxxxxxx
是高 16 位,作为key
。yyyyyyyyyyyyyyyy
是低 16 位,作为块内偏移量,即value
。
RoaringBitmap 通过这种分片机制将大范围的整数集合切分成多个小块,分别存储在不同的容器(container)中,从而实现了高效的存储和查询。
2. RoaringArray 的 key
和 value
在 RoaringArray
中,key
是一个 short
类型(16 位的整数),用于标识每个分片或块。value
则是一个指向具体容器的引用(Container
),用于存储属于该块的所有整数。
每一个 Container
都存储 0 到 65535 之间的整数(即 2^16 的范围),并使用不同的内部结构(如 ArrayContainer
、BitmapContainer
或 RunContainer
)来存储数据。Container
的类型取决于数据的密度,以选择合适的存储方式。
具体来说:
key
: 表示块的高 16 位(用于区分不同的块)。value
: 一个Container
实例,包含块内的所有数据。
3. RoaringArray
的内部实现
RoaringArray
本质上是一个包含两个数组的类:
keys
: 一个short
数组,存储每个块的key
。values
: 一个Container
数组,存储与key
对应的容器。
RoaringArray
维持了 keys
数组的排序,以便在执行操作时可以通过二分查找快速定位目标块。例如,在查找或添加一个新的整数时,RoaringArray 可以通过二分查找快速定位相应的 key
,并使用对应的 Container
存取数据。
public class RoaringArray implements Cloneable {
// `keys`数组:存储块的key
protected short[] keys;
// `values`数组:存储对应的容器(ArrayContainer, BitmapContainer, RunContainer等)
protected Container[] values;
// 当前数组中存储的元素数量
protected int size;
// 构造函数
public RoaringArray() {
this.keys = new short[INITIAL_CAPACITY];
this.values = new Container[INITIAL_CAPACITY];
this.size = 0;
}
// 添加新的(key, value)对到RoaringArray
public void append(short key, Container value) {
ensureCapacity(size + 1);
keys[size] = key;
values[size] = value;
size++;
}
// 二分查找,找到对应key的位置
public int binarySearch(short key) {
int low = 0;
int high = size - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
short midVal = keys[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}
// 通过key找到对应的Container
public Container getContainer(short key) {
int index = binarySearch(key);
return index >= 0 ? values[index] : null;
}
}
4. Container
的具体实现
Container
是 RoaringBitmap 中的一个接口,用于存储每个块中的具体数据。根据块中整数的密度和分布情况,RoaringBitmap 采用不同的 Container
实现来优化存储和访问效率:
- ArrayContainer:当一个块中的整数数量较少时,使用
ArrayContainer
。它将整数存储在一个short
数组中,适合稀疏的情况。 - BitmapContainer:当一个块中包含较多的整数时,使用
BitmapContainer
,它使用位图来存储整数,适合密集的情况。 - RunContainer:当块中的整数序列包含大量连续的数据时,使用
RunContainer
,它将整数范围压缩存储,适合长序列的情况。
这些 Container
类实现了相同的接口,因此可以根据数据分布动态选择最合适的容器,从而减少内存占用并提高访问效率。
例如,ArrayContainer
的实现:
public class ArrayContainer extends Container {
private short[] array; // 存储实际的整数
private int cardinality; // 记录当前元素数量
public ArrayContainer() {
this.array = new short[INITIAL_CAPACITY];
this.cardinality = 0;
}
// 添加元素到ArrayContainer
public void add(short value) {
if (cardinality == array.length) {
increaseCapacity();
}
array[cardinality++] = value;
}
}
5. 操作流程示例
我们将从一个实际的例子入手,详细说明 ArrayContainer
是如何在 RoaringBitmap 中存储和读取数据的。
假设我们有一个 RoaringBitmap,其中存储了一些整数集合 {1, 2, 3, 65536, 65537, 65538}
。这些整数经过 RoaringBitmap 的分片机制会被分为不同的块,每个块包含 65536 个连续的整数。ArrayContainer
将负责其中某些块的数据存储和管理。
这些整数将被划分为两个不同的块:
-
第一个块: 包含
{1, 2, 3}
。- 对于整数
1, 2, 3
,它们的高 16 位是0
,低 16 位分别是1, 2, 3
。 - 因此,
key = 0
,表示该整数属于第一个块。
- 对于整数
-
第二个块: 包含
{65536, 65537, 65538}
。- 对于整数
65536, 65537, 65538
,它们的高 16 位是1
,低 16 位分别是0, 1, 2
。 - 因此,
key = 1
,表示该整数属于第二个块。
- 对于整数
使用 ArrayContainer 存储数据
RoaringBitmap 使用 RoaringArray
来存储不同块的 key
和 value
,其中 value
是指向具体容器(Container)的引用。在这个例子中,我们会为 key = 0
和 key = 1
分别分配一个 ArrayContainer
来存储对应的整数。
1. 存储 {1, 2, 3}
到 ArrayContainer
假设 RoaringArray
中的 key = 0
对应一个 ArrayContainer
,该容器会存储块内偏移量 [1, 2, 3]
。以下是具体的存储步骤:
- 初始化一个
ArrayContainer
。 - 将低 16 位的值依次添加到
ArrayContainer
中,即1
,2
,3
。 ArrayContainer
的内部数组会存储[1, 2, 3]
。
ArrayContainer arrayContainer1 = new ArrayContainer();
arrayContainer1.add((short) 1);
arrayContainer1.add((short) 2);
arrayContainer1.add((short) 3);
2. 存储 {65536, 65537, 65538}
到 ArrayContainer
同理,对于 key = 1
,RoaringArray
会分配另一个 ArrayContainer
,并存储块内偏移量 [0, 1, 2]
:
ArrayContainer arrayContainer2 = new ArrayContainer();
arrayContainer2.add((short) 0);
arrayContainer2.add((short) 1);
arrayContainer2.add((short) 2);
ArrayContainer 的内部存储结构
每个 ArrayContainer
内部存储的数据结构如下:
arrayContainer1
中保存[1, 2, 3]
。arrayContainer2
中保存[0, 1, 2]
。
在 RoaringArray 中映射 key
到 ArrayContainer
RoaringArray 将 key = 0
和 key = 1
分别映射到 arrayContainer1
和 arrayContainer2
:
RoaringArray roaringArray = new RoaringArray();
roaringArray.append((short) 0, arrayContainer1);
roaringArray.append((short) 1, arrayContainer2);
读取数据
假设我们想检查整数 65537
是否存在于 RoaringBitmap 中。以下是查找过程的步骤:
-
分离高低 16 位:
- 取出整数
65537
的高 16 位:1
,这就是key
。 - 低 16 位是
1
,这是块内偏移量。
- 取出整数
-
查找对应的 Container:
- 在
RoaringArray
中找到key = 1
的位置。 - 获取对应的
ArrayContainer
,即arrayContainer2
。
- 在
-
在 ArrayContainer 中查找偏移量:
- 在
arrayContainer2
中查找1
。 arrayContainer2
包含[0, 1, 2]
,因此找到了1
,说明整数65537
存在于 RoaringBitmap 中。
- 在
代码示例:存储和读取
完整的存储和读取过程代码如下:
public class RoaringBitmapExample {
public static void main(String[] args) {
// 创建 RoaringArray 并初始化 ArrayContainer
RoaringArray roaringArray = new RoaringArray();
// 初始化第一个块(key = 0)的 ArrayContainer
ArrayContainer arrayContainer1 = new ArrayContainer();
arrayContainer1.add((short) 1);
arrayContainer1.add((short) 2);
arrayContainer1.add((short) 3);
roaringArray.append((short) 0, arrayContainer1);
// 初始化第二个块(key = 1)的 ArrayContainer
ArrayContainer arrayContainer2 = new ArrayContainer();
arrayContainer2.add((short) 0);
arrayContainer2.add((short) 1);
arrayContainer2.add((short) 2);
roaringArray.append((short) 1, arrayContainer2);
// 检查整数 65537 是否存在
int target = 65537;
short key = (short) (target >>> 16); // 高 16 位
short offset = (short) (target & 0xFFFF); // 低 16 位
// 在 RoaringArray 中查找 key 对应的 ArrayContainer
Container container = roaringArray.getContainer(key);
if (container != null && container.contains(offset)) {
System.out.println("整数 " + target + " 存在于 RoaringBitmap 中");
} else {
System.out.println("整数 " + target + " 不存在于 RoaringBitmap 中");
}
}
}
输出结果
数 65537 存在于 RoaringBitmap 中
总结
RoaringArray
的key
: 用于标识 RoaringBitmap 中的不同块,表示整数的高 16 位。RoaringArray
的value
: 指向对应的Container
,存储块内所有的整数,表示整数的低 16 位。Container
实现: 不同的Container
实现(ArrayContainer、BitmapContainer、RunContainer)使得 RoaringBitmap 可以根据块内数据密度选择最合适的存储结构,从而实现高效存储和快速操作。
通过这种分片存储结构和不同类型的容器选择,RoaringBitmap 实现了对大量整数数据的高效存储和快速集合操作。