具有快速连续范围检索的数据结构

Data structure with fast contiguous ranges retrieval

本文关键字:检索 数据结构 范围 连续      更新时间:2023-10-16

想象一下数据结构,它操纵一些连续的容器,并允许快速检索该数组中包含数据(可能还有空闲范围)的连续索引范围。让我们把这个范围称为"块"。每个区块都知道它的头和尾索引:

struct Block
{
    size_t begin;
    size_t end;
}

当我们操纵数组时,我们的数据结构会更新块:

    array view          blocks [begin, end]
--------------------------------------------------------------
0 1 2 3 4 5 6 7 8 9     [0, 9]
pop 2                   block 1 splitted
0 1 _ 3 4 5 6 7 8 9     [0, 1] [3, 9]
pop 7, 8                block 2 splitted
0 1 _ 3 4 5 6 _ _ 9     [0, 1] [3, 6] [9, 9]
push 7                  changed end of block 3
0 1 _ 3 4 5 6 7 _ 9     [0, 1] [3, 7] [9, 9]
push 5                  error: already in
0 1 _ 3 4 5 6 7 _ 9     [0, 1] [3, 7] [9, 9]
push 2                  blocks 1, 2 merged
0 1 2 3 4 5 6 7 _ 9     [0, 7] [9, 9]

甚至在分析之前,我们就知道块检索速度将是应用程序性能的基石。基本用途是:

  • 经常检索连续块
  • 非常罕见的插入/删除
  • 大多数时候,我们希望块的数量是最小的(防止碎片)

我们已经尝试过:

  1. std::vector<bool>+std::list<Block*>。每次更改时:将true/false写入vector,然后遍历它进行循环并重新生成list。在块的每个查询上返回CCD_ 5。比我们想要的要慢。

  2. std::list<Block*>直接更新列表,因此不需要遍历。退货清单。要调试/测试的代码很多。

问题:

  1. 这个数据结构有通用名称吗
  2. 是否已经实现(调试和测试)了这样的数据结构
  3. 如果没有,您对快速、稳健地实现此类数据结构有何建议

对不起,我的解释不太清楚。

编辑

此容器的典型应用程序是管理缓冲区:系统或GPU内存。在GPU的情况下,我们可以在单顶点缓冲区中存储大量数据,然后更新/无效一些区域。在每次绘制调用中,我们必须知道要绘制的缓冲区中每个有效块的第一个和最后一个索引(通常,每秒十分之一到数百次),有时(每秒一次)我们必须插入/删除数据块。

另一个应用程序是自定义的"块内存分配器"。为此,类似的数据结构在"Alexandrescu A.-现代C++设计"一书中通过侵入式链表实现。我正在寻找更好的选择。

我在这里看到的是一个简单的二进制树
您有具有beginend索引的对(块),也就是说,对(a,b),其中a <= b。因此块集可以很容易地排序并存储在搜索二叉树中
搜索与给定数字相对应的块很容易(只是典型的二进制树搜索)。因此,当您从数组中删除一个数字时,您需要搜索与该数字对应的块,并将其拆分为两个新块请注意,所有块都是叶子,内部节点是两个子节点形成的间隔
另一方面,插入意味着搜索块,并测试它的兄弟,以知道兄弟是否必须崩溃。这应该通过树递归地完成。

您可能想要尝试类似树的结构,可以是简单的红黑树,也可以是B+树。

您的第一个解决方案(布尔向量+块列表)似乎是一个不错的方向,但请注意,您不需要完全从头开始重新生成列表(或遍历整个向量)-您只需要遍历列表,直到找到新更改的索引应该固定的位置,并拆分/合并列表上适当的块。

如果列表遍历太长,您可以实现一个块向量,其中每个块都映射到其起始索引,每个孔都有一个块来说明孔的结束位置。您可以像列表一样快速遍历此矢量,因为您总是跳到下一个块(一个O(1)查找确定块的末尾,另一个O)查找确定下一块的开头。然而,好处是您还可以直接访问索引(用于推送/弹出),并通过二进制搜索找出它们的封闭块。为了使它发挥作用,你必须对"孔"进行一些维护工作(像真正的块一样合并和分割它们),但在任何插入/删除时也应该是O(1)。重要的是块之间总是有一个孔,反之亦然

为什么要使用块列表?你需要稳定的迭代器和稳定的引用吗?boost::stable_vector可能会有所帮助。如果你不需要稳定的引用,也许你想要的是编写一个包装容器,其中包含一个std::vector块和一个size blocks.caccepty()的辅助内存映射,它是迭代器索引(保存在返回的迭代器中,到块向量中的实际偏移量)和一个当前未使用的迭代机索引列表的映射。

每当您从块中擦除成员时,您都会重新打包块并相应地打乱映射,以提高缓存一致性,而当您想要插入时,只需将其推回块即可。

使用块打包,当以删除速度为代价进行迭代时,可以获得缓存一致性。并保持相对较快的插入时间。

或者,如果您需要稳定的引用和迭代器,或者容器的大小非常大,以一定的访问速度、迭代速度和缓存一致性为代价,您可以将向量中的每个条目封装在一个简单的结构中,该结构包含实际条目和到下一个有效条目的偏移量,或者只将指针存储在向量中,并在删除时将其设置为null。