支持< O(n)求和元素0到n的查询

时间:2010-11-22 23:04:58

标签: arrays data-structures tree sum

例如,假设您按照给定的顺序在列表中包含以下数字:

list = [4, 10, 3, 5, 1]

所以list [0] == 4,list [4] == 1。

现在假设您需要一个求和查询,它将告诉您所有先前值到该给定位置的总和。

list.sum(0) == 4
list.sum(1) == 14
list.sum(2) == 17
list.sum(3) == 22
list.sum(4) == 23

此外,我希望以下操作,同时仍然保持总和查询不变:

list.swap(0, 1) // swap the two positions
list == [10, 4, 3, 5, 1]
list.slideBefore(0, 3) // slides 1st position value to before the 2nd position
list == [4, 3, 10, 5, 1]
list.slideAfter(2, 3) // slide 1st position value to after 2nd position
list == [4, 3, 5, 10, 1]
list.replace(3, 9) // replace value at 1st param with literal value 2nd param
list == [4, 3, 5, 9, 1]
list.append(17) // adds value to end
list == [4, 3, 5, 9, 1, 17]

这可以通过数组轻易处理。但总和查询总是O(n)。我希望找到一个数据结构,将总和查询保持在O(1)或O(lg n),同时还将上述操作保持在O(1)或O(lg n)。

我相信我可以操纵fast array数据结构来完成我想要的,但我还没有完全解决。

我看到的另一个数据结构是Fenwick树,但我不清楚它是否会起作用。

有任何建议,想法,技巧或提示吗?

2 个答案:

答案 0 :(得分:3)

考虑一个简单的数组,您可以将总和存储到此元素而不是元素。 就这样,

int sum(int n){ 
    return array[n]; // O(1) !
};

int elem(int n){
    if (n)
        return array[n] - array[n-1];
    return array[0];
};

除了replace之外的所有操作都会有O(1)次,这将需要O(n)。

您还可以考虑一个二叉树,它只在叶子中保存值,并在每个节点中保留其子节点的总和。

答案 1 :(得分:1)

您要使用的数据结构将在很大程度上取决于您的访问模式。如果查询非常频繁且修改操作很少,那么您可以只保留一个“脏”标志,并在设置“脏”标志时重新计算查询总和。

然后,您可以通过设置“脏索引”来改进它,该索引保存已更改的最低项目的索引。在查询时,您必须重新计算该项目的总和以及之后的所有项目。或者,也许,只有你需要总和的项目,此时你可以更新“脏指数”。

如果查询频繁且修改不频繁,或者模式经过大量修改后会进行大量查询,那么这种懒惰的评估会非常有效。

'swap'和'append`可以在O(1)时间内完成,并且如果它们还没有脏,则不会“弄脏”总和。 'replace'当然会导致在该索引处设置脏索引(当然,前提是它没有处于较低的索引)。

如果您的数据结构是数组,那么

slidebeforeslideafter本质上是O(N),因为您必须移动数组中的数据。在您的示例中,您有:

list == [10, 4, 3, 5, 1]
list.slideBefore(0, 3) // slides 1st position value to before the 2nd position
list == [4, 3, 10, 5, 1]

因此,数组中的项目1和2必须向左移动一个位置,以便为项目0重新定位腾出空间。如果您有slideBefore(0, 1000),则阵列中的1,000个项目必须向上移动一个位置。如果这些操作频繁且列表很大,您可能需要不同的底层表示。

另一种可能性是“列表清单”实施。想象一下包含20个项目的列表,这些项目分为4个子列表,每个子项列表包含5个项目。每个子列表都维护项目的计数以及项目中的项目总和。子列表中的每个节点都维护列表中所有项目的运行总和。更新项目时,只需更新该项目子列表的总和。同样,如果你使用lazy evaulation,你只需要重新计算以下子列表的总和,如果有人查询它。

要处理插入和删除,请允许子列表在拆分之前增长到某个最大值。说你的“理想”是每个子列表五个项目。但是在将它分成两个子列表之前,你允许它增长到10。对于删除,您可以允许子列表转到0,或者如果子列表中的项目少于3个,则可以将其与上一个或下一个子列表组合。

理想的子列表大小取决于您希望列在列表中的项目总数,以及您希望遇到的操作组合。本质上为O(N)的操作(如删除和滑动)将有利于较小的子列表,但随后重新计算会变得更加昂贵,因为您有更多的子列表。

这并没有真正改变算法的运行时复杂性(也就是说,O(n / 5)仍然被认为是O(N)),但它确实改变了实际的运行时一点点。对于中等大小的列表,它可能是一个真正的胜利。