关联性math:(a + b)+ c!= a +(b + c)

最近,我正在浏览Eric Lippert的一篇旧博客文章,他在写关于关联性的文章时提到,在C#中, (a + b) + c对a,b的某些值不等于a + (b + c) , C。

我无法弄清算术值的types和范围可能是真的,为什么。

doubletypes的范围内:

 double dbl1 = (double.MinValue + double.MaxValue) + double.MaxValue; double dbl2 = double.MinValue + (double.MaxValue + double.MaxValue); 

第一个是double.MaxValue ,第二个是double.Infinity

double精度型上:

 double dbl1 = (double.MinValue + double.MaxValue) + double.Epsilon; double dbl2 = double.MinValue + (double.MaxValue + double.Epsilon); 

现在dbl1 == double.Epsilon ,而dbl2 == 0

在字面上阅读这个问题:-)

checked模式下:

 checked { int i1 = (int.MinValue + int.MaxValue) + int.MaxValue; } 

i1int.MaxValue

 checked { int temp = int.MaxValue; int i2 = int.MinValue + (temp + temp); } 

(注意使用tempvariables,否则编译器会直接给出一个错误…从技术上讲,这将是一个不同的结果:-)编译正确vs不编译)

这会抛出一个OverflowException …结果是不同的:-)( int.MaxValue vs Exception

一个例子

 a = 1e-30 b = 1e+30 c = -1e+30 

其他答案展示了如何使用极小数和大数得到不同的结果,下面是一个例子,其中带有现实正常数的浮点数给出了不同的答案。

在这种情况下,而不是在精度的极限使用数字,我只是做了很多补充。 (((...(((a+b)+c)+d)+e)......(((a+b)+(c+d))+((e+f)+(g+h)))+...

我在这里使用python,但如果你用C#编写这个结果,你可能会得到相同的结果。 首先创build一个百万个值的列表,全部为0.1。 把它们从左边加起来,你会发现舍入误差变得很重要:

 >>> numbers = [0.1]*1000000 >>> sum(numbers) 100000.00000133288 

现在再次添加它们,但这次成对添加它们(有更有效的方式来做这个,使用较less的中间存储,但我保持简单的实现):

 >>> def pair_sum(numbers): if len(numbers)==1: return numbers[0] if len(numbers)%2: numbers.append(0) return pair_sum([a+b for a,b in zip(numbers[::2], numbers[1::2])]) >>> pair_sum(numbers) 100000.0 

这一次舍入误差被最小化。

编辑完整性,这是一个更有效但不太容易遵循配对总和的实现。 它给出与上面的pair_sum()相同的答案:

 def pair_sum(seq): tmp = [] for i,v in enumerate(seq): if i&1: tmp[-1] = tmp[-1] + v i = i + 1 n = i & -i while n > 2: t = tmp.pop(-1) tmp[-1] = tmp[-1] + t n >>= 1 else: tmp.append(v) while len(tmp) > 1: t = tmp.pop(-1) tmp[-1] = tmp[-1] + t return tmp[0] 

这里是用C#编写的简单的pair_sum:

 using System; using System.Linq; namespace ConsoleApplication1 { class Program { static double pair_sum(double[] numbers) { if (numbers.Length==1) { return numbers[0]; } var new_numbers = new double[(numbers.Length + 1) / 2]; for (var i = 0; i < numbers.Length - 1; i += 2) { new_numbers[i / 2] = numbers[i] + numbers[i + 1]; } if (numbers.Length%2 != 0) { new_numbers[new_numbers.Length - 1] = numbers[numbers.Length-1]; } return pair_sum(new_numbers); } static void Main(string[] args) { var numbers = new double[1000000]; for (var i = 0; i < numbers.Length; i++) numbers[i] = 0.1; Console.WriteLine(numbers.Sum()); Console.WriteLine(pair_sum(numbers)); } } } 

输出:

 100000.000001333 100000 

这源于普通的值types(int,long等)使用固定数量的字节存储的事实。 当两个值的总和超过字节存储容量时,溢出是可能的。

在C#中,可以使用BigInteger来避免这种问题。 BigInteger的大小是任意的,因此不会产生溢出。

BigInteger仅适用于.NET 4.0及更高版本(VS 2010+)。

简短的答案是(a + b) + c == a + (b + c)math上,但不一定是计算上的。

记住,电脑真的是二进制工作,甚至简单的小数可以导致转换为内部格式的舍入误差。

根据语言的不同,甚至可能会导致舍入误差,在上例中, a+b的舍入误差可能与b+c的舍入误差不同。

一个令人惊讶的违法者是JavaScript: 0.1 + 0.2 != 0.3 。 舍入误差是十进制下的很长一段距离,但真实和有问题。

通过首先添加小部件来减less舍入误差是一个总体原则。 这样,他们可以在被大数目所淹没之前积累起来。