快速计算传入数字的最小值,最大值和平均值

时间:2012-04-23 20:49:08

标签: c# performance algorithm

计划每秒接收约50,000个号码。

在任何给定时刻,我需要计算在最后一秒(关于给定时刻)到达的值(数字)的最小值,最大值和平均值。

有没有办法在不使用数组或列表(缓冲区)存储到达的数字和计算结果的情况下执行此操作?

如果我需要使用缓冲区,那么实现这一目标的有效方法是什么?

(请注意,缓冲区中的数字也必须不时有效删除)

11 个答案:

答案 0 :(得分:21)

这是一种在某些情况下可以有效地提高效率的算法:

  1. 当事件进入时,请完全缓冲它们,并计算一个正在运行的sumcountminmax(无关紧要)。

  2. 当发出对averageminmax的请求时,从缓冲区后面循环并开始删除超过一秒的值。随时减去sumcount

    • 如果值均高于min,您可以保留min。如果值低于max,您可以保留max。在这种情况下,您可以有效地更新averageminmax

    • 如果值低于min或高于max,则需要循环遍历数组的其余部分并重新计算。

  3. 每秒执行一次左右两次,以便缓冲区不会过满。此代码也可以在每个缓冲区插入上执行,或者在任何有意义的地方执行。

  4. 这种工作的最佳结构是循环缓冲区,以避免内存分配和GC阻碍。它应该足够大,以涵盖每秒消息大小的最坏情况。

    <强>更新

    根据使用场景,另一件事要做的是运行上面的算法,但是以10 x 100ms块而不是1 x 1000ms块。也就是说,保持这10个块的运行最小值,最大值,总和和计数。然后,当您达到“无效”方案时,通常只需要查看最新的100毫秒数据或快速浏览其他9个块的最小值和最大值。


    @ ja72提供了一个好主意,如果它们失效,可以节省查找最小值和最大值:

    而不是保持最小值/最大值x_min,x_max而是保持它们在具有i_min和i_max的x [i]阵列中的位置的索引。然后找到它们有时可能很简单,但是当考虑的最后一个值保持最小值和最大值时,需要扫描整个列表以建立新的限制。


    Sam Holder在评论中有另一个好主意 - 保持一个始终排序的并行数组,这样可以让你从顶部或底部删除数字,以便更轻松地找到新的最小值和最大值。但是,这里的插入速度有点受损(需要按顺序排列)。


    最终,正确的选择取决于程序的使用特性。读取值与插入频率的频率是多少?

答案 1 :(得分:5)

使用循环缓冲区,每个元素都有时间戳和数据,每秒最大元素数作为循环缓冲区的大小。

当每个元素插入缓冲区头部时,检查缓冲区另一侧的到期时间,删除该元素。

如果删除的元素是最小值或最大值,则必须计算新的最小值/最大值。如果不是,您将根据新到货时间更新最低/最高。

对于平均值,保持总数,保持计数和除数。

答案 2 :(得分:3)

你不能把你的号码及其到达时间与当前的最大值和时间排在一起。队列中的最小值(可能需要保持相同最小值/最大值的值数)以及队列中所有数字的总值和元素数。

然后当一个数字到达时,将其添加到队列并调整最小值/最大值/值和计数。然后查看队列的另一端并删除所有不在最后一个数字到达后1秒内的元素,并再次调整最大/最小/计数/总值。

然后你不需要在瞬间计算任何东西,只需返回预先计算的东西(即读取当前最小值/最大值或总数/计数值)

正如@yaman所指出的,你不能只保留最小值和最大值,因为当你被删除时,你可能不知道新的。在这种情况下,我可能只保留列表中所有数字的第二个副本,而不是按到达时间排序,我会按值排序。然后,您只需添加和删除此列表中的每个数字,这样您就可以始终知道最大值和最小值。这样可以节省扫描缓冲区中的所有元素以查找新的最大值/分钟,但需要保留2份副本,但是对此列表的更新应该很便宜,因为它已经订购了。

答案 3 :(得分:2)

@DanRedux是正确的;你需要每次计算它们,因为你的输入正在改变。现在,您可能更愿意按需或预先计算这些数字(即,当您获得新批次时),具体取决于所需结果的频率。

例如,如果您的平均用例每隔约30秒轮询这些统计数据,那么我可能只是按需计算它们并缓存结果,直到新批次进入。它实际上归结为您的使用场景。 / p>

至于如何储存它们,你真的没有选择,是吗?内存中的所有50,000个数字都需要空间。所以...你需要一大块足以容纳它们的内存。为了避免每次新序列进入时不断分配2KB,你可能最好声明一个足够大的数组来保存最大的数据集并重新使用它。同样,这取决于您的要求,即,您知道您的最大可能数据集是什么吗?是否会在一段时间内分配新的内存块会导致应用程序出现问题?

答案 4 :(得分:2)

如果上一个Nx[0] .. x[N-1]的平均值为m_1x[0]是最新值,x[N-1]考虑的最后一个值)然后将值推回一个索引并添加值m_2的值的平均值x

 m_2 = m_1+(x-x[N-1])/N;
 for(i=N-1;i>0;i--) { x[i]=x[i-1]; }
 x[0] = x;

而不是保持最小/最大值x_minx_max而不是保留x[i]数组中i_mini_max所在位置的索引}。然后找到它们有时可能很简单,但是当考虑的最后一个值保持最小值和最大值时,需要扫描整个列表以建立新的限制。

答案 5 :(得分:2)

有一种有效的方法可以跟踪给定时间窗口内的最小值(或最大值),而通常必须存储已到达该窗口的所有数字。 (但是,最糟糕的情况仍然需要存储所有数字,因此您需要为所有数字保留空间或接受您有时可能会得到不正确的结果。)

诀窍是只存储以下值:

  1. 已到达时间窗口内,
  2. 小于(或大于)以后的任何值。
  3. 用于实现此目的的合适数据结构是存储值及其到达时间的简单循环缓冲区。您需要在缓冲区中维护两个索引。这是算法的简单英文描述:

    启动时:

    • 分配 N - 元素缓冲区val的值和相应的 N - 元素缓冲区time的时间戳。
    • imax = 0(或0到 N -1之间的任何其他值),并让inext = imax。这表示缓冲区当前为空。

    当及时收到新值 new t

    • imaxinexttime[imax]超出时间间隔时,请将imax增加1(模数 N )。
    • imaxinextval[inext-1]new时,将inext减1(模数 N )。
    • 允许val[inext] = newtime[inext] = t
    • 如果inextimax-1,请将inext增加1(模数 N );否则适当地处理“缓冲区满”条件(例如,分配更大的缓冲区,抛出异常,或者只是忽略它并接受最后一个值没有被正确记录)。

    请求最小值时:

    • imaxinexttime[imax]超出时间间隔时,请将imax增加1(模数 N )。
    • 如果imaxinext,请返回val[imax];否则返回一个错误,表明在该时间间隔内没有收到任何值。

    如果收到的值是独立且相同的分布(并作为泊松过程到达),我相信可以证明在任何给定时间列表中存储的平均值是ln( n +1),其中 n 是在时间间隔内收到的平均值。对于 n = 50,000,ln( n +1)≈10.82。但是,应该记住,这只是平均值,偶尔可能需要几倍的空间。


    对于平均值,遗憾的是同样的技巧不起作用。如果可能,您可以切换到exponentially moving average,可以使用非常小的空间轻松跟踪(只有一个数字用于平均值,一个时间戳用于指示上次更新的时间)。

    如果这不可能,但您愿意接受平均值的少量平滑,则可以计算平均值,例如每毫秒。这样,无论何时请求最后一秒的平均值,您都可以取平均最后1001毫秒的平均值,根据间隔内这些毫秒的多少来衡量最旧和最新的平均值:

    启动时:

    • interval 是平均时间间隔的长度,让 n 为子间隔的数量。
    • dt = interval / n
    • 分配 n +1元素缓冲区sum值和 n +1元素缓冲区cnt非负整数,并用零填充。
    • prev具有任何价值。 (这并不重要。)

    当及时收到新值 new t

    • i = floor(t / dt )mod( n +1)。
    • 如果iprev
      • sum[i]减去total,从cnt[i]减去count
      • sum[i] = 0,cnt[i] = 0,让prev = i
    • new添加到sum[i]并将cnt[i]增加一个。
    • new添加到total并将count增加一个。

    当时请求平均值 t

    • i = floor(t / dt )mod( n +1)。
    • 如果iprev
      • sum[i]减去total,从cnt[i]减去count
      • sum[i] = 0,cnt[i] = 0,让prev = i
    • j =(i - n )mod( n +1)=(i + 1)mod (名词 1)。
    • w = frac(t / dt )=(t / dt ) - floor({{1 } / dt )。
    • 返回(t - total×w)/(sum[j] - count×w)。

答案 6 :(得分:1)

可悲的是,没有。这是不可能的原因是因为你只需要考虑它们是第二个旧的,这意味着你必须每次重新计算结果,这意味着巨大的循环。

如果你想计算最后40,000个数字,或者所有数字,它会更容易,但由于它是基于时间的,你必须每次都遍历整个列表。

答案 7 :(得分:1)

  

有没有办法在不使用数组或列表(缓冲区)的情况下执行此操作   存储到达的数字并计算结果?

没有。如上所述,如果不存储信息,这样做是不可能的。您可以稍微调整一下这些要求,以消除对缓冲区的需求。

  

如果我需要使用缓冲区,那么有效的方法是什么   此?

您需要为此使用队列。

添加项目时,如果是新的最大值或最小值,则相应地调整这些变量。您可以通过公式here逐步调整均值。只需取新值,减去均值,除以集合中的新项目数(即队列大小加1),然后将其添加到旧均值中。

然后你会有或多或少的东西:

while(queue.Peek < oneSecondAgo)
{
  oldItem = queue.Peek
  queue.Dequeue();
  if(oldItem == min) //recalculate min
  if(oldItem == max) //recalculate max
  mean += SubtractValueFromMean(oldItem.Value, queue.Count);
}

要从平均值中删除值,您应该只能使用相同的公式进行添加,但使用值的负值而不是正值...我认为。一个更好的数学家可能需要帮助你。

答案 8 :(得分:1)

如果数字一个接一个地出现,那么使用秒表和一个while循环逐个获取每个数字一秒,然后计算min,max和avg。

double min = double.MaxValue;
double max = double.MinValue;
double sum = 0;
int count = 0;
double avg;
StopWatch sw = new StopWatch();
sw.Start();
while(sw.Elapsed.TotalSeconds <= 1)
{
   // Get the next number in the stream of numbers
   double d = GetNextNumber();

   // Calculate min
   if(d < min) min = d;
   // Calculate max
   if(d > max) max = d;

   // Calculate avg = sum/ count
   sum += d;
   count++;
}

avg = sum/count;

然后返回min,max和avg。

答案 9 :(得分:1)

如果不保留缓冲区或队列中的数字,就不可能做到。

原因很简单:当最大值到期时(超出1秒窗口),新的最大值是在最后一秒内到达的其他数字,因此您需要记录候选者可能成为新的最大值。

需要平均值意味着所有值在到期时都会产生影响,并且在一秒钟之前不会抛出任何值。

Sam Holder关于使用队列的建议是一个很好的建议,尽管您可能需要一个专门的队列,可以同时将您的列表保存在两个订单中:收到号码的顺序(到达时间),并从最大值到最小值。

使用具有两个下一个和前两个指针的单个节点对象(一对在时间上,另一对在大小方面)可以同时从两个列表中删除元素,当一个元素从时态列表到期时,你可以访问大小列表的指针,因为它们位于同一个节点对象中。

可以通过保持运行总计和运行计数来维护平均值,在删除元素时将其删除,并在创建时添加它们,因此不必每次都迭代整个列表以便计算平均值。

正如他们对Sam Holder的帖子中的评论中提出的那样,使用最大堆和最小堆比使用列表更有效,我们再次需要使用带有指针的单个节点用于堆和list,所以我们不必搜索要删除它们的元素,并且可能需要花一些时间考虑如何正确删除不在堆顶部的元素,同时保持O(log n)插入的保证和删除。

答案 10 :(得分:0)

平均而言,有3个案例需要考虑:

  1. 您的数字是整数。保持运行总数并计算,添加新的 值到总数,从总和中减去旧值,然后除以 根据需要按计数。这很简单,因为你不必这样做 担心失去精确度。
  2. 您的号码是浮点数,您需要丢失0 精度:您必须遍历整个一秒钟列表 计算平均值
  3. 你的数字是浮点数,你可以忍受一些损失 precision:对整数平均值进行操作,完成 每1000左右重新计算一次。
  4. 对于最小值和最大值(仅与上面的#1和#3相关):

    • 将值保存在以值为索引的treap中。
    • 同时将值保存在按时间排序的双向链表中。保存开头和结尾 清单。
    • 从列表的开头删除,并添加到结尾 列表。
    • 对于每个新值:将其添加到时间链接列表的开头。 根据需要从时间链表的末尾删除值。

    在链接列表中添加和删除值时,请在treap上执行相应的操作。要从treap获得最小值和最大值,只需在log(n)时间内执行find_minimum和find_maximum操作。当您在常量时间内从链接列表的右端删除内容时,也请在log(n)时间从treap中删除它们。

    Treaps可以在log(n)时间内找到它们的最小值,在log(n)时间内找到它们的最大值,并在log(n)时间内找到任意值。一般而言,访问数据所需的方式越多,处理得越好的数据结构就越好。