在恒定时间内更新连续数字序列的平均值

Posted

技术标签:

【中文标题】在恒定时间内更新连续数字序列的平均值【英文标题】:Update the average of a continuous sequence of numbers in constant time 【发布时间】:2014-05-24 20:03:12 【问题描述】:

如何在不必遍历整个列表的情况下平均加减数字?

这在许多情况下都非常有用。例如,连续计算流中最后 X 个值的平均值,将两个平均值相加,并根据新用户投票更新评分。

【问题讨论】:

这被称为incremental averaging,并在 Math.SE 上得到了回答。 【参考方案1】:

确实有可能在恒定时间 O(1) 内以平均值操作单个值。

以下函数将一个数字添加到平均值中。 average 是当前平均值,size 是当前平均值中的值数,value 是要添加到平均值中的数字:

double addToAverage(double average, int size, double value)

    return (size * average + value) / (size + 1);

同样,以下函数从平均值中删除一个数字:

double subtractFromAverage(double average, int size, double value)

    // if (size == 1) return 0;       // wrong but then adding a value "works"
    // if (size == 1) return NAN;     // mathematically proper
    // assert(size > 1);              // debug-mode check
    // if(size < 2) throw(...)        // always check
    return (size * average - value) / (size - 1);

您可能会考虑返回 0 作为一组大小为 0 的平均值,这样添加一个值就会将该值作为平均值。但是,如果您想将集合减小到 0 大小视为错误,则返回 NAN 会将其传播到将来的使用中,使其更加可见。但是请参阅What is the arithmetic mean of an empty sequence? - 您可能只想当场大声报告错误,或者抛出 C++ 异常(不只是引发 FP 异常),如果这是一个错误的话。

如果你没有特殊情况,你可能会从x / 0. 和非零x 中得到 + 或 -Inf,除非你删除的值正好等于当前平均值;然后你会得到0. / 0. => NaN。


您还可以组合这些功能来轻松替换数字。如果您要计算数组/流中最后 X 个数字的平均值,这将非常方便。

double replaceInAverage(double average, int size, double oldValue, double newValue)

    return (size * average - oldvalue + newValue) / size;

也可以在恒定时间内计算两个平均值的总平均值:

double addAveragesTogether(double averageA, int sizeA, double averageB, int sizeB)

    return (sizeA * averageA + sizeB * averageB) / (sizeA + sizeB);

【讨论】:

虽然addToAverage 是正确的,但请注意,使用此alternative formula 时,精度误差可能会更小。 如果size1subtractFromAverage 会抛出错误。我会添加if (oldSize == 1) return 0; @Yousif:我不确定静默返回0 是否适合所有用例。如果有的话,NaN 会更合适。 (当前代码实际上将返回+-Inf,这也不好,除非average == value 得到0. / 0. => NaN)。我想返回0 的好处是增加平均值会将平均值设置为该值。 还要注意FP除法相当昂贵;这通常仍然值得,但不如加法和乘法那么便宜。 (如果 size 是编译时常量,您可以使用 double inverse = 1. / size; 但这可能不准确,并且可能会在重复使用时累积错误。)【参考方案2】:

已经提到的典型方式是:

( n * a + v ) / (n + 1);

n 是我们的旧计数,a 是我们的旧平均值,v 是我们的新值。

但是,n * a 部分最终会溢出,因为n 变得更大,尤其是在a 本身很大的情况下。为避免这种使用:

a + ( v - a ) / (n + 1)

随着n 的增加,我们确实会损失一些精度 - 自然,我们正在将a 修改为连续更小的数量。批处理值可以缓解问题,但对于大多数任务来说可能是多余的。

【讨论】:

如果有人对为什么第二个等式也有效,你可以在这里找到一个很好的解释:math.stackexchange.com/a/1836447/709688 但是是否也有替代方法来移除和替换? 请注意,浮点数在所有尺度上都保持相同的相对精度,因此乘以然后除以相似大小的数字不会损失太多精度;只有当它实际上溢出超过 DBL_MAX 时才会出现问题,大约是 1.79769e+308,这是非常巨大的。另一个主要的数值问题是用n*a + va + v/n 将一个小数加到一个大数上。如果v/n 小于a 的1ULP,则添加它甚至不会翻转a 的尾数的低位。即如果|v| &lt; |a|/2^53 左右。即使v 不是那么小,您仍然可能会丢失其大部分精度。 @PeterCordes 是的,这将等式 2 与从头开始重新计算平均值进行比较。不过,等式 1 仍然存在同样的问题 - 当 n*a 接近 MAX 然后 n*a + v = n*a 时。使用合适的数据类型重新计算平均值总是会更好,但并不总是可能的(或必要的),就像在 OP 的情况下一样。 @Barnack 要从平均值中删除一个项目,请从当前平均值中删除该项目的影响,即a-(v-a)/(n-1)。 (其中na 表示删除v 之前的项目数和平均值)。

以上是关于在恒定时间内更新连续数字序列的平均值的主要内容,如果未能解决你的问题,请参考以下文章

如何构建具有连续值之间最大平均距离的阈值图?

和为S的连续正数序列

为啥哈希表平均具有恒定的访问时间?

Pandas 时间序列:常规 10 分钟窗口内不规则间隔数据的分组和滚动平均值

以相反的顺序打印数字的数字

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