【问题标题】:Data structure which supports < O(n) sum queries of elements 0 up to n支持 < O(n) 元素 0 到 n 的求和查询的数据结构
【发布时间】:2010-11-22 23:04:58
【问题描述】:

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

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

所以列表[0] == 4,列表[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 树,但我不清楚它是否可以工作。

有什么建议、想法、技巧或技巧吗?

【问题讨论】:

    标签: arrays data-structures tree sum


    【解决方案1】:

    考虑一个简单的数组,在其中存储此元素的总和而不是元素。 这样一来,

    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)。

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

    【讨论】:

    • 在这个结构下替换是O(n)。否则就很好了。
    • 我看不出 slideBefore 或 slideAfter 可能是 O(1)。例如,slideAfter(2,5) 将要求您重新计算项目 3、4 和 5 的总和。当然,“重新计算”是减去您移动的值的问题。如果是 slideAfter(2, 1002),您将重新计算 1,000 个值。此外,任何“滑动”操作本质上都是 O(N) 操作,因为您必须移动数组中的数据。
    • @Jim 是的,幻灯片不是 O(1),而是 O(count_of_slided_elems)。但是如果这个计数是恒定的并且不依赖于n,你可以说它是 O(1)。
    • 但是计数可以任意大(当然小于 n),所以称它为 O(1) 是不正确的。而且由于没有指定常数,我们必须假设它可以是小于 n 的任何数字,平均它将是 n/2,如果我记得我的算法类,它被认为是 O(N) .
    • @Jim 当然。只有 OP 知道估计的操作频率和参数。我只是给出了一个想法,而不是最终的解决方案。
    【解决方案2】:

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

    然后,您可以通过设置“脏索引”来优化它,该索引保存已更改的最低项目的索引。查询时,您必须重新计算该项目的总和,然后再计算。或者,也许只到您需要总和的项目,此时您可以更新“脏索引”。

    如果查询频繁而修改不频繁,或者模式是大量修改后有大量查询,这种惰性求值会非常有效。

    'swap' 和 'append' 可以在 O(1) 时间内完成,如果它们还没有脏,就不会“脏”这些总和。 “替换”当然会导致将脏索引设置在该索引处(当然,前提是它还没有位于较低的索引处)。

    slidebefore 和 slideafter 如果您的数据结构是数组,则本质上是 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 个项目。每个子列表维护项目的计数和其中项目的总和。子列表中的每个节点都维护列表中它之前的所有项目的运行总和。更新项目时,您只需更新该项目的子列表的总和。同样,如果您使用惰性求值,则只有在有人查询时才重新计算以下子列表的总和。

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

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

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

    【讨论】:

    • 我喜欢这个结论。 O(C*N) -&gt; C * O(N) -&gt; O(N)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-07
    • 1970-01-01
    • 2011-08-26
    • 1970-01-01
    相关资源
    最近更新 更多