序列平均值的高效数据结构

Posted

技术标签:

【中文标题】序列平均值的高效数据结构【英文标题】:Efficient data structure for the average of a sequence 【发布时间】:2013-03-19 21:09:24 【问题描述】:

我需要设计一种数据结构,可以有效地支持对存储的(我认为合适的)数字序列进行以下操作:

将整数 x 添加到序列的第一个 i 元素 在序列末尾追加一个整数k 删除序列的最后一个元素 检索序列中所有元素的平均值

示例

从一个空序列[]开始

追加 0 ([0]) 追加 5 ([0, 5]) 追加 6 ([0, 5, 6]) 将 3 添加到序列中的前 2 个元素 ([3, 8, 6]) 检索平均值 5.66 ([3, 8, 6]) 删除最后一个元素 ([3, 8]) 检索平均值 5.5 ([3, 8])

以前的工作

我考虑过使用Fenwick Trees (Topcoder Editorial),但为此我需要指定序列的最大大小来初始化 Fenwick 树,这不一定知道。但是,如果我有序列可以支持的最大元素数量,我可以支持O(lg N) 上的这些操作,如果我还保存序列中所有元素的总和。

编辑:问题是Codeforces problem,我需要所有操作的亚线性运行时间,因为在最坏的情况下,添加到第一个元素可能是相同的作为添加到整个序列

【问题讨论】:

这是干什么用的?第一次手术很不寻常。 我正在尝试解决Codeforces 上的一个问题一段时间,但我可以只使用树,但由于树的数组初始化,解决方案显然太慢了(我认为) 【参考方案1】:

您是否考虑过使用链表加上当前长度和总和?对于每个操作,您可以通过不断的额外工作来维持当前平均值(您知道列表的长度和总和,并且所有操作都会以恒定的方式更改这两个值)。

唯一的非常量操作是将常数添加到任意前缀,这将花费与前缀大小成正比的时间,因为您需要调整每个数字。

要使所有操作保持不变(摊销)不变需要更多的工作。不使用双向链表,而是使用堆栈返回数组。数组中的每个插槽i 现在都包含i 处的数字和要添加到i 之前的每个元素的常量。 (请注意,如果您说“将 3 添加到元素 11 之前的每个元素”,则插槽 11 将包含数字 3,但插槽 0-10 将为空。)现在每个操作都与以前一样,除了附加一个新元素涉及标准的数组加倍技巧,当您从队列末尾弹出最后一个元素时,您需要 (a) 在该插槽中添加常量,以及 (b) 将插槽 i 中的常量值添加到插槽i-1 的常量。所以对于你的例子:

追加 0:[(0,0)], sum 0, length 1

附加 5:([(0,0),(5,0)], sum 5, length 2

附加6:[(0,0),(5,0),(6,0)], sum 11, length 3

将 3 添加到序列中的前 2 个元素:[(0,0),(5,3),(6,0)], sum 17, length 3

检索平均值 5.66

删除最后一个元素[(0,0),(5,3)], sum 11, length 2

检索平均值 5.5

删除最后一个元素[(0,3)], sum 3, length 1

这里有一些 Java 代码可能更清楚地说明了这个想法:

class Averager 
  private int sum;
  private ArrayList<Integer> elements = new ArrayList<Integer>();
  private ArrayList<Integer> addedConstants = new ArrayList<Integer>();

  public void addElement(int i) 
    elements.add(i);
    addedConstants.add(0);
    sum += i;
  

  public void addToPrefix(int k, int upto) 
    addedConstants.set(upto, addedConstants.get(upto) + k);
    sum += k * (upto + 1);
    // Note: assumes prefix exists; in real code handle an error
  

  public int pop() 
    int lastIndex = addedConstants.length() - 1;

    int constantToAdd = addedConstants.get(lastIndex);
    int valueToReturn = elements.get(lastIndex);
    addedConstants.set(
      lastIndex-1,
      addedConstants.get(lastIndex-1) + constantToAdd);
    sum -= valueToReturn;
    elements.remove(lastIndex);
    addedConstants.remove(lastIndex);
    return valueToReturn + constantToAdd;
    // Again you need to handle errors here as well, particularly where the stack
    // is already empty or has exactly one element
  

  public double average() 
    return ((double) sum) / elements.length();
  

【讨论】:

我希望所有操作至少是对数的,因为前缀可能是一个非常大的列表的长度。要使其保持不变,还需要做哪些额外的工作? @GustavoTorres 我可能遗漏了一些东西,但我看不出在列表的i 元素中添加的东西怎么可能少于O(i) 诀窍是懒惰地做,依赖于这样一个事实,即真正观察队列中的值的唯一方法是将它们弹出或取它们的平均值。请参阅代码示例。 @jacobm 非常好的实现!简单高效!谢谢! @GustavoTorres 但是如何获得对任意元素的恒定时间更新,以便添加到任意前缀?【参考方案2】:

听起来像 Doubly Linked List,维护了头部和尾部引用,以及当前的总和和计数。

将整数 x 添加到序列的前 i 个元素中

从 *head 开始,添加 x,下一项。重复i 次。 sum += i*x

在序列末尾追加一个整数 k

从 *tail 开始,用 head = tail,tail = null 创建新项目。相应地更新 *tail、sum 和 count。

删除序列的最后一个元素

将 *tail 更新为 *tail->prev。更新总和,减少计数

检索平均值 5.5 ([3, 8])

返回总和/计数

【讨论】:

+1 虽然您可以使用单链表来做到这一点。不需要双链接。只保留头部和尾部指针。 如果没有指向前一个元素的链接,删除最后一个元素会变成 O(n),对吧?必须知道更新 *tail 到什么 不,你需要两者——你需要能够在恒定时间内找到倒数第二个,并且你需要能够在与时间成正比的时间内迭代任意大小的前缀前缀的大小。 啊,你是对的。必须有那个反向链接才能删除最后一个。 您可以使用二叉索引树 (log(n))。另一种选择是一次回答多个查询。例如。如果您有以下 (i, x) 对 = (1,1), (2, 1), (3, 1),那么您将经历并添加 1, 2, 3 仅执行 3 次操作而不是执行 3 +2+1 操作。 (不好的例子,但是是的)。【参考方案3】:

这个数据结构可以只是一个元组 (N, S),其中 N 是计数,S 是总和和一堆数字。没有什么花哨。所有操作都是 O(1),除了第一个是 O(i)。

【讨论】:

【参考方案4】:

我建议你尝试使用Binary Indexed Tree。

它们允许您访问 O(Log(n)) 中的累积频率。

您也可以按 log(i) 顺序添加到前 i 个元素。

但是,不要将前 i 个元素增加 X,只需将第 n 个元素增加 X。

要删除最后一个元素,可能需要另一棵树来计算累计删除的数量。 (所以不是删除,而是将该数量添加到另一棵树中,访问第一棵树时总是从结果中减去)。

对于追加,我建议你从大小为 2*N 的树开始 这会给你空间。然后,如果你得到大于 2*N 的大小,请添加另一棵大小为 2*N 的树。 (不完全确定最好的方法,但希望你能弄清楚)。

【讨论】:

所以不是将第 i 个元素增加 X,而是将第 i 个元素增加 Xi?但是,如果这是列表中的最后一个元素,然后将其删除,会发生什么情况?你失去了 iX,当你真的只应该失去 X + 无论最后一个元素是什么。那么总和就不正确了。 你将第 n-i 个元素增加 x。这将元素 n-i、n-i+1、.. n 的累积频率增加了 x。实际总和是累积频率的总和,但是如果你自己跟踪它,它是 O(1)。【参考方案5】:

为了满足第一个要求,您可以维护一个单独的添加操作数据结构。基本上,它是范围和增量的有序集合。您还可以维护这些添加的总和。因此,如果您在前三项中添加 5,然后在前 10 项中添加 12,您将拥有:

3, 5
10, 12

这些加法的总和是(3*5) + (10*12) = 135。

当被问及总和时,您提供项目的总和加上这些加法的总和。

您遇到的唯一麻烦是删除列表中的最后一项。然后,您必须浏览此添加内容集合以找到包含最后一项(您要删除的项)的任何内容。该数据结构可以是哈希映射,键是索引。所以在上面的例子中,你的哈希映射是:

key: 3  value: 5
key: 10 value: 12

每当您执行第一个操作时,您都会检查哈希映射以查看是否已经存在具有该键的项目。如果是这样,您只需更新那里的值而不是添加新的增量。并相应地更新总和。

有趣。您甚至不必保留额外的总和。您可以随时更新总金额。

当您从列表中删除最后一项时,您会检查哈希映射以查找具有该键的项。如果有,则删除该项目,减少键,然后将其添加回哈希映射(或使用该键更新现有项目,如果有的话)。

所以,使用 mattedgod 提出的双向链表,加上他提出的总和。然后使用此哈希映射来维护列表中添加的集合,并相应地更新总和。

【讨论】:

【参考方案6】:

第 174 轮问题设置者发表了本轮的社论。你可以找到它here。您还可以查看一些公认的解决方案:Python、C++。

【讨论】:

当然这里的最优解是每个操作的O(1)。如果您仍然不明白,我可以尝试更彻底地解释它,但我认为给定的解决方案非常简单。 我确实阅读了教程,但他们的解释对我来说不是很清楚,而且大多数解决方案的代码都很神秘。虽然python解决方案确实很不错

以上是关于序列平均值的高效数据结构的主要内容,如果未能解决你的问题,请参考以下文章

时间序列数据库OpenTSDB设计

数据结构学习第二十三天

在数据帧时间序列的每月序列中查找给定月份的历史季节性平均值

基于不规则时间序列数据计算规则周期平均值的最佳方法

pyspark:使用自定义时间序列数据的滚动平均值

如何获取具有多列的时间序列数据框中的每小时平均值