为什么十进制小数不能在二进制中被精确表示?

314
在SO上发布了几个关于浮点表示的问题,例如小数0.1没有精确的二进制表示,因此使用“==”运算符将其与另一个浮点数进行比较是危险的。我理解浮点表示背后的原理。但我不明白的是,从数学角度讲,为什么小数点右侧的数字比左侧的数字更加“特殊”?例如,数字61.0具有精确的二进制表示,因为任何数字的整数部分始终是精确的。但数字6.10就不是精确的了。我只是把小数点向右移动了一位,突然间从精确到不精确。在数学上,这两个数字之间应该没有内在的区别--它们只是数字而已。相反,如果我向左移动小数点一位,得到数字610,我仍然处于精确状态。我可以一直朝着这个方向走(6100、610000000、610000000000000),它们仍然是精确的。但是一旦小数点超过某个阈值,数字就不再精确了。发生了什么?编辑:为了澄清,我想避免讨论诸如IEEE等行业标准表示法,并坚持我认为的数学“纯粹”方式。 在十进制中,位值是:
... 1000  100   10    1   1/10  1/100 ...

在二进制中,它们将会是:

... 8    4    2    1    1/2  1/4  1/8 ...

这些数字也没有任何限制。数字的位置从左到右无限增加。


2
你可能会发现这篇文章有助于理解浮点数内部的运作方式:浮点数的解剖 - John D. Cook
61
在二进制中,数字3表示为2¹+2°=2+1。非常简单易懂。现在,看一下1/3。你如何使用负幂次的2来表示它?试试实验,你会发现1/3等于无限序列2^-2 + 2^-4 + 2^-6 + 2^-8 + ...的总和,也就是说,在二进制中很难精确地表示。 - Lars Haugseth
26
Jon Skeet在你的问题中提供了很好的回答。但有一件事情被遗漏了,那就是你实际上问了两个不同的问题。标题问题是“为什么十进制小数不能在二进制中精确表示?”答案是,它们可以被表示。在你的标题和正文之间,你混淆了“二进制”和“浮点表示”的概念。浮点数是一种以固定数量的二进制数字表示十进制数的方法,但会牺牲精度。而二进制只是另一种计数法,可以表示任何十进制数,只要有无限多的数字。 - Chris Blackwell
3
有一些系统具有精确的十进制表示,其工作方式基本上与您描述的相同。 SQL十进制类型就是一个例子。LISP语言内置了这个功能。有几个商用和开源库可用于使用精确的小数计算。只是目前还没有此类硬件支持,大部分语言和硬件都实现了IEEE标准,以32或64位表示无限数量的数字。 - nos
2
该问题似乎与数学有关(即使它是涉及编程的数学),更适合在 [math.se] 上进行讨论。 - Cole Tobin
22个回答

393

如果你有足够的空间,十进制数可以被精确地表示 - 只是不能用浮点二进制小数来表示。如果使用浮点十进制点类型(例如.NET中的System.Decimal),那么许多在二进制浮点中无法精确表示的值可以精确表示。

换个角度看问题 - 在你熟悉的十进制系统中,你无法精确表示1/3,它是0.3333333...(循环)。你无法用浮点二进制小数表示0.1的原因正是完全相同的原因。你可以精确表示3、9和27,但无法精确表示1/3、1/9或1/27。

问题在于3是10的质因子。当你想把一个数字乘以3时,这不是问题:你总是可以通过整数乘法来避免问题。但是当你除以一个质数且该质数不是你基数的因子时,就可能会遇到麻烦(如果你试图将1除以该数,则一定会遇到问题)。

虽然0.1通常被用作最简单的确切十进制数的示例,但可以说0.2是更简单的例子,因为它是1/5 - 而5是在十进制和二进制之间引起问题的质数。


解决有限表示问题的副注:

一些浮点十进制类型像 System.Decimal 一样有固定大小,而其他的如 java.math.BigDecimal 则是“任意大”的,但它们总会达到某个极限,无论是系统内存还是数组的理论最大大小。然而,这是本答案的一个完全不同的观点。即使您有真正任意数量的位可以使用,在浮点二进制点表示中仍然无法精确表示小数0.1。将其与另一种情况进行比较:给定任意数量的十进制数字,您可以精确表示任何可以作为浮点二进制点的确切表示的数字。

31
例如,数字61.0在二进制下具有精确的表示,因为任何数的整数部分都是精确的。但数字6.10不是精确的。我所做的只是将小数点向左移动一位,突然间从精确的Exactopia转变成了不精确的Inexactville。数学上说,这两个数字之间本应该没有任何内在的区别——它们只是数字。 让我们暂时离开十进制和二进制的细节。让我们问一下,在基数b下,哪些数字具有有限的表示,哪些数字没有?仔细思考一下,我们可以知道一个数字x有一个有限的b表示,当且仅当存在一个整数n,使得x b^n是一个整数。
因此,例如,数字x=11/500具有一个有限的10表示,因为我们可以选择n=3,然后x b^n=22,是一个整数。然而,数字x=1/3没有,因为无论我们选择什么样的n,都无法摆脱3。
第二个例子提示我们去思考因子,我们可以看到对于任何有理数x=p/q(假设已经化简为最简形式),我们可以通过比较bq的质因数分解来回答这个问题。如果q有任何不在b的质因数分解中的质因子,我们将永远无法找到一个合适的n来摆脱这些因子。
因此,在基数10下,任何p/q,其中q有除2或5以外的质因子,都不会有有限的表示。所以现在回到十进制和二进制的基础上,我们可以看出任何有限十进制表示的有理数都是p/q的形式,当且仅当 q 的质因数分解只包含25;同样地,这个数在二进制中有限表示当且仅当q的质因子分解只包含2

但是其中一个情况是另一个情况的子集! 每当

q 的质因数分解只包含2

显然也成立:

q 的质因数分解只包含25

或者说,每当p/q在二进制下有限表示时,在十进制下p/q也一定有限表示。 然而反过来却不成立-每当q的质因数分解中有一个5,它将具有有限的十进制表示,但没有有限的二进制表示。 这就是其他答案提到的0.1例子。

所以我们得到了你问题的答案-因为2的质因数是10的质因数的子集,所有以2为终止的数字都是以10为终止的数字,但反之则不成立。 这与61和6.1无关-而是10和2的关系。

最后说明一下,如果由于某种怪癖人们使用(比如)17进制而我们的计算机使用5进制,你的直觉永远不会被误导 - 没有(非零,非整数)同时在这两种情况下具有有限表示的数字!


那么为什么“alert(0.15*0.15)”会显示“0.0225”呢? - Michael Geiser
7
简短回答:在显示时进行四舍五入。当作为 IEEE double 存储时,你认为是“0.15”的实际值为“0.149999999999999994448884876874”。可参考 jsfiddle - AakashM
很好的清晰代码示例!我希望我能为此点赞!我必须尝试一些函数来探索舍入截止点在哪里。我仍然惊讶于我们实际上必须处理这种垃圾;因为人们几乎100%的时间都使用十进制,而我们大部分时间都使用非整数,你会认为浮点数学的默认实现会处理这种无聊的事情。 - Michael Geiser
1
@MichaelGeiser 使用二进制的电路比使用十进制的电路更小、更快、更节能。今天我们可能可以证明这种额外开销是有意义的,但在20世纪70年代制定标准时,这是一个大问题。如果没有处理器电路的直接支持,尝试进行操作会更糟糕,速度差异将达到数量级的巨大差异。 - Mark Ransom
2
这个答案比Jon Skeet本人解释得更好! - goelakash
1
本答案解释了如何严谨地检查一个数字 x 是否具有终止的 b-表示。 - Jingguo Yao

17

根本(数学)原因在于,当您处理整数时,它们是可数无限的。

这意味着,即使有无限多个整数,我们也可以“数出”序列中的所有项,而不会跳过任何一项。这意味着如果我们想要获取列表中第610000000000000个位置上的项,我们可以通过公式计算出来。

然而,实数是不可数无限的。你不能说“给我第610000000000000个位置上的实数”,并得到一个答案。原因是因为,在考虑浮点值时,在01之间甚至存在无限数量的值。对于任何两个浮点数,情况也是如此。

更多信息:

http://en.wikipedia.org/wiki/Countable_set

http://en.wikipedia.org/wiki/Uncountable_set

更新:非常抱歉,我似乎误解了问题。我的回答是关于为什么我们不能表示每个真实值,我没有意识到浮点数会自动归类为有理数。


6
实际上,有理数是可数无限的。但并非每个实数都是有理数。我肯定可以产生一个确切小数序列,最终可以达到你想给我的任何确切小数。只有当你需要处理无理数时,才会涉及到不可数无限集合。 - Jon Skeet
是的,我应该说“实数”,而不是“浮点数”。我会澄清的。 - TM.
2
在我的看法中,逻辑变得不那么适用的时候是什么时候呢?因为我们不仅不能使用二进制浮点数来处理所有的实数,而且我们甚至不能处理所有的有理数(比如0.1)。换句话说,我认为这与可数性没有任何关系 :) - Jon Skeet
@jonskeet 我知道不同意Jon Skeet会违反自然法则,所以我当然不会这样做 :) 但是,我认为将数字的内部表示视为要表示外部的值集合的索引是可以的。通过这种思路,您可以看到,无论您的索引列表有多大(即使您拥有无限精度的位数),您仍然无法表示所有实数。 - TM.
4
@TM:但是OP并不试图表示所有实数。他试图表示所有精确的十进制数字,这是有理数的一个子集,因此只有可数无限个。如果他使用无限位作为十进制浮点类型,那么就没有问题了。使用这些位作为二进制浮点类型来表示十进制数字会导致问题。 - Jon Skeet
1
@molf 非常好的观点。我想我误解了问题,将其理解为“为什么我们可以表示任何整数值,却不能表示任何分数值”。 - TM.

13
重复我在评论中对Skeet先生说的话:我们可以用十进制表示1/3、1/9、1/27或任何有理数。我们通过添加一个额外的符号来实现。例如,对于数字的小数展开式中重复的数字,在数字上方加一条线。为了将十进制数字表示为二进制数字序列,我们需要1)一系列二进制数字,2)一个基数点和3)其他一些符号来指示序列的重复部分。

Hehner引号表示法 是一种做到这一点的方法。他使用引号符号来表示序列的重复部分。文章:http://www.cs.toronto.edu/~hehner/ratno.pdf 和维基百科页面:http://en.wikipedia.org/wiki/Quote_notation

没有什么规定我们不能向表示系统添加符号,所以我们可以使用二进制引号表示法精确地表示十进制比率,反之亦然。


那个符号系统在我们知道循环开始和结束的情况下是有效的。人类很擅长检测循环。但是,一般来说,计算机不行。为了能够有效地使用重复符号,计算机必须能够在进行计算后找出循环出现的位置。例如对于数字1/3,循环从一开始就开始了。但是对于数字1/97,循环直到你至少计算出96位数字的答案才会出现。(实际上,你需要96*2+1=193位数字才能确定。) - Barry Brown
4
实际上,对于计算机来检测循环并不难。如果您阅读 Hehner 的论文,他描述了如何检测各种算术运算的循环。例如,在使用重复减法的除法算法中,当您看到曾经见过的差异时,您就知道循环从哪里开始了。 - ntownsend
3
这个问题涉及到精确表示数字。有时候,精确表示需要很多位二进制数。引号表示法的优美之处在于,Hehner证明平均来说使用引号表示法可以比标准的32位定长表示法减少31%的表示大小。 - ntownsend

6

BCD - 二进制编码十进制 - 表示法是精确的。虽然它们不太能节省空间,但在这种情况下,这是您必须做出的权衡。


2
BCD并不比其他进位制更精确。例如:在BCD中如何精确表示1/3?你无法做到。 - Jörg W Mittag
15
BCD 是 DECIMAL 的一个精确表示,因此它的名字中包含“十进制”这个词。1/3 也没有一个精确的十进制表示。 - Alan

5

(注:我会在这里附加“ b”以表示二进制数字。所有其他数字均以十进制给出)

一种思考方式是使用类似科学计数法的方法。我们习惯于看到用科学计数法表示的数字,例如6.022141 * 10^23。浮点数使用类似的格式-尾数和指数来存储内部数据,但使用2的幂而不是10的幂。

您的61.0可以重写为1.90625 * 2^5,或者使用尾数和指数将其重写为1.11101b * 2^101b。要将其乘以十并(移动小数点),我们可以执行以下操作:

(1.90625 * 2^5)*(1.25 * 2^3)=(2.3828125 * 2^8)=(1.19140625 * 2^9)

或者使用二进制中的尾数和指数:

(1.11101b * 2^101b)*(1.01b * 2^11b)=(10.0110001b * 2^1000b)=(1.00110001b * 2^1001b)

请注意我们如何乘以这些数字。我们乘以尾数并添加指数。然后,由于尾数大于二,我们通过提高指数来规范化结果。这就像我们在十进制科学计数法中对数字进行操作后调整指数一样。在每种情况下,我们使用二进制中有限的表示来处理值,因此基本乘法和加法运算产生的输出值也会产生具有有限表示的值。

现在,考虑如何将61除以10。我们将从除以尾数1.90625和1.25开始。在十进制中,这会给出1.525,一个不错的短数字。但是,如果我们将其转换为二进制,它是多少呢?我们将按照通常的方式进行-尽可能减去最大的2的幂,就像将整数十进制转换为二进制一样,但我们将使用负2的幂:

1.525         - 1*2^0   --> 1
0.525         - 1*2^-1  --> 1
0.025         - 0*2^-2  --> 0
0.025         - 0*2^-3  --> 0
0.025         - 0*2^-4  --> 0
0.025         - 0*2^-5  --> 0
0.025         - 1*2^-6  --> 1
0.009375      - 1*2^-7  --> 1
0.0015625     - 0*2^-8  --> 0
0.0015625     - 0*2^-9  --> 0
0.0015625     - 1*2^-10 --> 1
0.0005859375  - 1*2^-11 --> 1
0.00009765625...
哎呀,现在我们有麻烦了。当用二进制表示1.90625/1.25=1.525时,它是一个重复的小数:1.11101b/1.01b=1.10000110011...b。我们的计算机只能存储有限的位数来保存尾数,并且会将该小数四舍五入并假定某个点后面全是零。当你将61除以10时看到的误差就是:

1.100001100110011001100110011001100110011...b * 2^10b
和:
1.100001100110011001100110b * 2^10b

正是因为尾数的四舍五入导致了我们与浮点值相关的精度损失。即使尾数可以准确地表示(例如,仅添加两个数字),如果尾数需要太多位才能适应归一化指数,则仍然可能出现数字损失。

事实上,当我们将十进制数字舍入到可管理的大小并只给出前几个数字时,我们经常这样做。因为我们用十进制表示结果,所以感觉很自然。但是如果我们将一个小数舍入并将其转换为不同的基数,那么它看起来就像由于浮点舍入而得到的小数一样丑陋。


5

这是一个好问题。

你的问题都基于“我们如何表示数字?”

所有数字都可以用十进制表示或二进制(2的补码)表示。 所有数字!!

但是一些数字(其中大多数)需要无限数量的元素(对于二进制位置,“0”或“1”,或对于十进制表示,“0”到“9”)。

比如十进制表示中的1/3(1/3 = 0.3333333... <- 有无限数量的“3”)

比如二进制中的0.1(0.1 = 0.00011001100110011.... <- 有无限数量的“0011”)

一切都在这个概念中。由于您的计算机只能考虑有限的数字集合(十进制或二进制),因此只有一些数字可以在您的计算机中被准确地表示...

正如Jon所说,3是一个质数,不是10的因子,因此1/3不能用基数为10的有限元素表示。

即使使用任意精度算术,基于2的编号位置系统也无法完全描述6.1,尽管它可以表示61。

对于6.1,我们必须使用另一种表示方法(例如十进制表示或IEEE 854,它允许使用基数2或基数10表示浮点值)


1
你可以将1/3表示为分数本身。你不需要无限数量的位来表示它。你只需将其表示为分数1/3,而不是取1并除以3的结果。几个系统都是这样工作的。然后,您需要一种使用标准/ * + -和类似运算符来处理分数表示的方法,但这很容易 - 您可以用笔和纸执行这些操作,教计算机执行它也不是什么大问题。 - nos
我在谈论“二进制(2的补码)表示法”。因为,当然,使用其他表示法可能会帮助您用有限数量的元素表示某些数字(而对于其他一些数字,您将需要无限数量的元素)。 - ThibThib

4
如果使用浮点数构造足够大的数字(因为它可以进行指数运算),那么在小数点前面也会出现不精确性。因此,我认为你的问题并不完全正确,因为前提是错误的。移位10次并不总是会产生更多精度,因为在某个时候,浮点数将不得不使用指数表示数字的大小,并以此失去一些精度。

4

这是因为在十进制中,你无法精确地表示1/3,需要写成0.33333(3)的形式。在二进制中也是同样的问题,只不过出现在不同的数字集合中。


3
我很惊讶没有人提到这一点:使用连分数。任何有理数都可以用这种方式在二进制中有限地表示出来。
一些例子:
1/3(0.3333 ...)
0; 3

5/9 (0.5555...)

0; 1, 1, 4

10/43 (0.232558139534883720930...)

0; 4, 3, 3

9093/18478 (0.49209871198181621387596060179673...)

在这里显示了一个比例,表示为分数和小数形式。
0; 2, 31, 7, 8, 5

从这里开始,有许多已知的方法可以在内存中存储一系列整数。

除了完全准确地存储您的数字外,连分数还具有其他一些好处,例如最佳有理逼近。如果您决定提前终止连分数的数字序列,则剩余的数字(重新组合为一个分数)将给出最佳可能的分数。这就是找到 pi 的近似值的方法:

圆周率的连分数:

3; 7, 15, 1, 292 ...

将序列在1处终止,得到分数:

355/113

这是一个非常好的有理数近似。


但是你如何用二进制表示它呢?例如,15需要4位来表示,但292需要9位。硬件(甚至软件)如何知道每个位之间的位边界在哪里?这是效率与准确性之间的权衡。 - ardent

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接