有效管理内存中的数据页面

时间:2013-09-17 07:52:06

标签: algorithm

我正在寻找一个合适的结构来处理以下问题:

  • 应用程序正在接收(例如从Web服务器)可变大小的数据页面Pi,例如页面P1可以包含20个元素,P2 3,P3 20,P4 20等......
  • 每个页面包含具有全局唯一递减Id j的元素T j,例如P2 = [T150,T149,T120]。在此示例中,P1 Tj元素ID严格低于120,P3元素严格大于150。
  • 这意味着Pi中的i不代表网络接收顺序,而是代表最终页面顺序,这在我们收到页面时是未知的,并且在插入新页面时可能会发生变化。

可以按任何顺序接收这些页面。一组页面P1..P10的示例:

  1. 首先是P3然后是P2然后是P1
  2. 然后是P6,然后是P5,然后是P4
  3. 然后是P10,然后是P9
  4. 然后是P8,然后是P7(请注意,在插入这些页面之前,P10和P9将是第8页和第9页)。
  5. 我想找到一个允许我执行以下操作的结构:

    • 在页面序列的中间,结尾或开头的任何位置插入新页面(例如,在P9和P6之间插入P8和P7),因此根据内部Tj元素。但我正在寻找比O(n)更好的复杂性。
    • 删除页面也很不错。
    • 有趣的部分是查询:我希望能够根据间隔进行查询:例如从第15个元素到第25个元素。在首先给出的例子中,我将检索P1的最后5个元素+ P2的3个+ P3的第一个元素。当然,在这里,我期待的复杂性比O(n)......
    • 更好

    基本上,我想要实现的是在接收推文时有效地存储在推文的内存页面中(Twitter timeline)。我当然可以使用数组或链接列表但这意味着O(n)插入和查询时间......当然,我需要能够根据列表中的“位置”查询项目以显示它们在ListView中。

    我想了几个解决方案,但没有一个是合适的:

    • 首先,间隔树,但它们允许插入和查询元素的“相同范围”,即插入“j”但查询“j”但不查询“i”。我不确定我可以根据“i”附加一种前缀和。
    • 我的目的是使用Fenwick树来存储页面累积的页数,Pi中的i是树中的“位置”,它代表值Tj所关联的键。但Fenwwick树不适合插入新元素......我想知道是否可以用红黑树实现Fenwick树,但我不确定...
    • 另一种解决方案可能是摆脱页面并直接在一种B树中插入元素我猜。但是如果我想一次插入包含许多元素的页面,我会对速度有点关注。

    我希望我的问题得到明确说明。关于可能有效扩展的有效解决方案的想法吗?

    编辑:我想查询不在内部项目ID上的页面(例如T140,T150或其他任何内容),而是查询Element(即Tweet)索引。例如,在我的第一个例子中,T120将是第21个项目(因为有一个包含20个元素的页面P1)。所以我希望能够查询一个间隔[20-29],它应该返回元素[T120,...]。我不想直接搜索120.

1 个答案:

答案 0 :(得分:2)

您可以使用线程平衡二叉搜索树。但是,在搜索中,您不会针对节点x中的单个号码检查号码n,而是针对页面px检查页面pn。由于您的页面不重叠,因此非常简单。在您选择的pxx)中输入一个ID,然后根据pnpn_minpn_max的最小值和最大值进行检查。然后:

if pn_min <= x <= pn_max
    the page you are looking for
if x < pn_min
    go left
if x > pn_max
    go right

为了能够检索某个范围内的ID,您首先要在树中找到该范围的最小值(x)(使用普通搜索)。如果它不存在,则意味着您已经搜索到了一片叶子。称之为pn

if x < pn_min
    start from pn
if x > pn_max
    start from pn->next

其中pn->next是线程树中的下一个节点。现在你有了一个起始页面。只需浏览页面并检索ID,直到达到范围的最大值。如果页面结束,请转到线程树中的next页面并继续如上所述。

由于树是平衡的,这应该在搜索/插入/删除操作中为您提供O(logn),并且由于它是线程化的,它应该为您提供O(logn + k)(其中k是区间查询中的给定范围内的ID。


注意:您的树不需要在两个方向都有线程。 GNU's libavl似乎有你需要的工具,但如果它更简单,或者你必须自己编写,你可以只考虑一个右线程树。


修改:根据rs ID查询范围。

稍作修改,你也可以做到这一点。该算法与查找实际ID的范围相同,只是查找第一个元素是不同的。

让我们在每个节点上附加一个数字,说明在该节点左侧插入了多少个ID。请拨打此pn_before。另外,请将pn_size称为pn中的ID数。现在搜索rth id(这是[rth, sth] ID范围内的第一个)如下:

passed = 0
pn = root
while pn not leaf
    if passed + pn_before < rth <= passed + pn_before + pn_size
        the node you are looking for
    if rth <= passed + pn_before
        go left
    if rth > passed + pn_before + pn_size
        passed += pn_before + pn_size
        go right

要解释什么是passed,请想象以下树

          __________ p3 {5, 6} before: 4___________
         /                                         \
  ______p2 {2, 3, 4} before: 1              _______p5 {9}: before 2_____
 /                                         /                            \
p1 {1} before: 0                          p4 {7, 8}: before 0           p6 ...

现在假设你正在寻找第7个元素(在这个例子中也有id 7)。如果你查看根(p3),你会看到它前面有4个ID,其中有2个ID。因此,第3个如果适用,你就是正确的。现在在这个新树中,你知道你已经传递了4 + 2个ID,所以你不必寻找第7个元素,而是寻找第1个元素。变量passed有助于跟踪右转时跳过的id。

或者,您可以从pn_before缩小pn_sizerth,因此rth每次都会变小。它是一样的(但记得备份rth,因为你以后需要它)

找到rth元素的位置后,您将继续作为上一个间隔查询算法。

现在只剩下问题是更新pn_before。这很简单。由于每个子树的每个根只跟踪它的左子树,然后在插入/删除时,你需要向上到树的根并添加/删除该节点的pn_before数量刚插入/删除的ID。记得只改变从左边孩子上去的父母。如果您去找父母并且您是正确的孩子,则父母不需要跟踪您。请注意,在这种情况下,您不应该停止,因为父级可能是其父级的左子级。

在纸上做,你会得到它;)

另一个注意事项:重新平衡树时要注意pn_before

再次搜索O(logn + k),其中k是您要查询的时间间隔内的ID数(s - r)。插入/删除后向后退的附加步骤不会更改这些算法的顺序,因为后退步骤也是O(logn)