RoaringBitmap 使用详解

        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 的范围),并使用不同的内部结构(如 ArrayContainerBitmapContainer 或 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. 第一个块: 包含 {1, 2, 3}

    • 对于整数 1, 2, 3,它们的高 16 位是 0,低 16 位分别是 1, 2, 3
    • 因此,key = 0,表示该整数属于第一个块。
  2. 第二个块: 包含 {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 中,即 123
  • 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 = 1RoaringArray 会分配另一个 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 中。以下是查找过程的步骤:

  1. 分离高低 16 位

    • 取出整数 65537 的高 16 位:1,这就是 key
    • 低 16 位是 1,这是块内偏移量。
  2. 查找对应的 Container

    • 在 RoaringArray 中找到 key = 1 的位置。
    • 获取对应的 ArrayContainer,即 arrayContainer2
  3. 在 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 实现了对大量整数数据的高效存储和快速集合操作。

猜你喜欢

转载自blog.csdn.net/goTsHgo/article/details/143480412