【发布时间】:2014-05-24 20:03:12
【问题描述】:
如何在不必遍历整个列表的情况下平均加减数字?
这在许多情况下都非常有用。例如,连续计算流中最后 X 个值的平均值,将两个平均值相加,并根据新用户投票更新评分。
【问题讨论】:
-
这被称为incremental averaging,并在 Math.SE 上得到了回答。
标签: performance math iteration time-complexity average
如何在不必遍历整个列表的情况下平均加减数字?
这在许多情况下都非常有用。例如,连续计算流中最后 X 个值的平均值,将两个平均值相加,并根据新用户投票更新评分。
【问题讨论】:
标签: performance math iteration time-complexity average
确实有可能在恒定时间 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 时,精度误差可能会更小。
size 是1,subtractFromAverage 会抛出错误。我会添加if (oldSize == 1) return 0;
0 是否适合所有用例。如果有的话,NaN 会更合适。 (当前代码实际上将返回+-Inf,这也不好,除非average == value 得到0. / 0. => NaN)。我想返回0 的好处是增加平均值会将平均值设置为该值。
size 是编译时常量,您可以使用 double inverse = 1. / size; 但这可能不准确,并且可能会在重复使用时累积错误。)
已经提到的典型方式是:
( n * a + v ) / (n + 1);
n 是我们的旧计数,a 是我们的旧平均值,v 是我们的新值。
但是,n * a 部分最终会溢出,因为n 变得更大,尤其是在a 本身很大的情况下。为避免这种使用:
a + ( v - a ) / (n + 1)
随着n 的增加,我们确实会损失一些精度 - 自然,我们正在将a 修改为连续更小的数量。批处理值可以缓解问题,但对于大多数任务来说可能是多余的。
【讨论】:
1.79769e+308,这是非常巨大的。另一个主要的数值问题是用n*a + v 或a + v/n 将一个小数加到一个大数上。如果v/n 小于a 的1ULP,则添加它甚至不会翻转a 的尾数的低位。即如果|v| < |a|/2^53 左右。即使v 不是那么小,您仍然可能会丢失其大部分精度。
n*a 接近 MAX 然后 n*a + v = n*a 时。使用合适的数据类型重新计算平均值总是会更好,但并不总是可能的(或必要的),就像在 OP 的情况下一样。
a-(v-a)/(n-1)。 (其中n 和a 表示删除v 之前的项目数和平均值)。