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

时间:2013-08-20 22:48:02

标签: c++ algorithm data-structures

想象一下数据结构,它操纵一些连续的容器,并允许快速检索此数组中包含数据(也可能是空闲范围)的连续索引范围。我们称这个范围为“块”。每个块都知道它的头尾指数:

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,然后遍历for循环并重新生成list。在块的每个查询返回list。比我们想要的要慢。

  2. std::list<Block*>直接更新列表,所以没有遍历。退货清单。很多代码要调试/测试。

  3. 问题:

    1. 该数据结构是否具有一些通用名称?
    2. 是否已经实施(调试和测试)了这样的数据结构?
    3. 如果不是,您可以就快速,稳健地实施此类数据结构提出哪些建议?
    4. 很抱歉,如果我的解释不太清楚。

      修改

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

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

4 个答案:

答案 0 :(得分:4)

您可能想尝试一种树状结构,可以是简单的红黑树或B +树。

答案 1 :(得分:4)

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

答案 2 :(得分:1)

你的第一个解决方案(bool向量+块列表)似乎是一个很好的方向,但请注意,你不需要从头开始完全重新生成列表(或者遍历整个向量) - 你只需要遍历列表,直到找到新修改索引的位置,并拆分/合并列表中的相应块。

如果列表遍历证明太长,则可以实现块的向量,其中每个块都映射到其起始索引,并且每个孔都有一个块表示孔的结束位置。您可以像列表一样快地遍历此向量,因为您总是跳转到下一个块(一个O(1)查找以确定块的结束,另一个O(1)查找以确定下一个块的开始。但是你也可以直接访问索引(对于push / pop),并用二进制搜索找出它们的封闭块。 为了使它工作,你将不得不对“漏洞”进行一些维护工作(合并并将它们像实际块一样拆分),但在任何插入/删除时也应该是O(1)。重要的是,块之间总是有一个洞,反之亦然

答案 3 :(得分:0)

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

无论何时从块中擦除成员,都会重新打包块并相应地对地图进行洗牌以提高缓存一致性,当您想要插入时,只需将push_back移动到块中。

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

或者,如果您需要稳定的引用和迭代器,或者如果容器的大小非常大,以某些访问速度,迭代速度和缓存一致性为代价,您将向量中的每个条目包装在一个简单的结构中包含实际条目和下一个有效的偏移量,或者只是在向量中存储指针,并在删除时将它们保留为空。