(五)浮點(diǎn)數(shù)(誰偷了你的精度?)

2018-02-24 16:09 更新

光棍節(jié)加長版

如果我告訴你,中關(guān)村配置最高的電子計(jì)算機(jī)的計(jì)算精度還不如一個(gè)便利店賣的手持計(jì)算器,你一定會(huì)反駁我:「今天寫博客之前又忘記吃藥了吧」。

你可以用最主流的編程語言計(jì)算?0.2 + 0.4,如果你使用的是 Chrome、FireFox、IE 8+,可以按 F12 鍵,然后找到 「控制臺(tái)」,輸入上面的?表達(dá)式?0.2 + 0.4,回車。

然后再用最簡陋的計(jì)算器(如果你沒有手持計(jì)算器沒關(guān)系,手機(jī)、電腦都自帶一個(gè)計(jì)算器,打開“運(yùn)行”,輸入calc,回車) 再計(jì)算一下剛才的?算式?0.2 + 0.4。

怎么樣?同意我的觀點(diǎn)了吧!?再簡陋的計(jì)算器也比超級(jí)計(jì)算器的精度高,關(guān)鍵不在于它的頻率和內(nèi)存,而在于它是如何設(shè)計(jì)、如何表示、如何計(jì)算的

不能表示 VS 不能精確表示

在上一章『浮點(diǎn)數(shù)(從驚訝到思考)』中我們講到用浮點(diǎn)數(shù)表示?數(shù)?時(shí)出現(xiàn)的問題——很多數(shù)都?不能表示。(注意浮點(diǎn)數(shù)表示的是數(shù),而不僅僅是小數(shù)。)

如果你數(shù)學(xué)比較好,或者你確信你身體健康,沒有心臟病、高血壓,沒有受過重大精神創(chuàng)傷,那我告訴你, 在浮點(diǎn)數(shù)的表示范圍內(nèi),有多于 99.999...% 的數(shù)在計(jì)算機(jī)中是?不能表示?的。 真的是太令人吃驚,也太令人遺憾了。 真相總是很殘忍。

請注意我使用的措辭,區(qū)別開?不能表示?和?不能精確表示。

下面我從數(shù)量級(jí)分析一下,32bit 浮點(diǎn)數(shù)的表示范圍是 10 的 38 次方,而表示個(gè)數(shù)呢,是 10 的 10 次方。 能夠被表示的數(shù)只有 1/100000000.... (大概有30個(gè)零),這個(gè)數(shù)多大呢?還記得那個(gè)國際象棋和麥子的故事嗎?

為了讓你了解?指數(shù)的威力,我再舉個(gè)例子:

有一張很大很大的紙,對(duì)折 38 次,會(huì)有多高呢? 一米?一百米?比珠峰還高?再次考驗(yàn)?zāi)阈呐K承受能力的時(shí)刻到了:它不僅僅比珠峰高,其實(shí)它已經(jīng)快到達(dá)月球了。

回到原來的話題,還有更殘忍的真相。 在剩下的可以表示的不到 0.000...1% 的數(shù)中,又有多少不能精確表示呢?這就是我寫這篇博客的目的。

上一章中我還給出了一種用定點(diǎn)數(shù)精確表示小數(shù)的方法。 事實(shí)上,手持計(jì)算器、java 中的 BigDecimal、C# 中的貨幣類型、MySQL 中的 NUMERIC 類型就是這么干的。 你還記得在數(shù)據(jù)庫中添加字段時(shí)的 SQL 語句是如何寫的嗎?現(xiàn)在明白為什么我說?再簡陋的計(jì)算器也比超級(jí)計(jì)算器的精度高?了吧。

這篇博客我將為大家講解為什么很多數(shù)?不能精確表示,本篇可能比較燒腦子,我會(huì)盡量用最通俗的語言,最貼近現(xiàn)實(shí)的例子來講解,不在乎篇幅有多長,關(guān)鍵是要給大家講明白。下一篇,你將了解到浮點(diǎn)數(shù)如何工作,以及為什么很多數(shù)?不能表示。

熱身?—— 問:要把小數(shù)裝入計(jì)算機(jī),總共分幾步?你猜對(duì)了,3 步。

  • 第一步:轉(zhuǎn)換成二進(jìn)制
  • 第二步:用二進(jìn)制科學(xué)計(jì)算法表示
  • 第三步:表示成 IEEE 754 形式

在上面的第一步和第三步都有可能?丟失精度。

十進(jìn)制 VS 二進(jìn)制

下面我們討論如何把十進(jìn)制小數(shù)轉(zhuǎn)換成二進(jìn)制小數(shù)(什么?你不會(huì)?請自覺去面壁)。

考慮我們將 1/7(七分之一) 寫成小數(shù)的時(shí)候是如何做的?

用 1 除以 7,得到的商就是小數(shù)部分,剩下的余數(shù)我們繼續(xù)除以 7,一直除到什么時(shí)候結(jié)束呢? 有兩種情況:

  1. 如果余數(shù)為 0。yeah!終于結(jié)束了,洗洗睡吧

  2. 當(dāng)除到某一步時(shí),余數(shù)等于 1… 停!stop!等一下,我發(fā)現(xiàn)有什么地方怪怪的。余數(shù)為 1,余數(shù)如果為 1 的話,再繼續(xù)除下去,不就又是 1/7 了嗎?繞了一個(gè)大彎,又回來了?對(duì),你猜的很對(duì),它永遠(yuǎn)不會(huì)結(jié)束,它循環(huán)了。

注意我上面說的 情況2,我們判斷他循環(huán),并?不是從直觀看感覺它重復(fù)了,而是因?yàn)?在計(jì)算過程中,它又回到了開頭**。為什么這么說呢?當(dāng)你計(jì)算一個(gè)分?jǐn)?shù)時(shí),它總是連續(xù)出現(xiàn) 5,出現(xiàn)了好多次,例如 0.5555555… 你也無法斷定它是無限循環(huán)的,比如 一億分之五。

記得高中時(shí),從一本數(shù)學(xué)課外書學(xué)到了手動(dòng)開平方的方法,于是很興奮的去計(jì)算 2 的平方根,發(fā)現(xiàn)它的前幾位是 1.414,哇,原來「2的平方根」等于 1.414141…。很多天以后,當(dāng)我再次看到我的筆記時(shí),只能苦笑了,「2的平方根」不可能循環(huán)啊,它可是一個(gè)無理數(shù)啊。

你可能不耐煩了,嘰哩哇啦說這么多,有用嗎?當(dāng)然有用了,以后如果 MM 問你:你會(huì)愛我到什么時(shí)候?你可以回答她:我會(huì)愛你到 1/7 的盡頭。難道我會(huì)把我的表白方式告訴你們嗎??我對(duì)你的愛就像圓周率,無限——卻永不重復(fù)。

扯遠(yuǎn)了,現(xiàn)在會(huì)到主題。 你也許會(huì)說:我明白了,循環(huán)小數(shù)不能精確表示,放到計(jì)算機(jī)中會(huì)丟失精度; 那么有限小數(shù)可以精確表示吧,比如 0.1。

對(duì)于無限小數(shù),不只是計(jì)算機(jī)不能精確表示,即使你用別的辦法(省略號(hào)除外),比如紙、黑板、寫字板…都無法精確表示。什么?手機(jī)?也不能,當(dāng)然不能了。不,不,iPad也不行,1萬買的也不行,真的,再貴的本子也寫不下。

哪些數(shù)能精確表示?

那么 0.1 在計(jì)算機(jī)中可以精確表示嗎?

答案是出人意料的,?不能。

在此之前,先思考個(gè)問題:?在 0.1 到 0.9 的 9 個(gè)小數(shù)中,有多少可以用二進(jìn)制精確表示呢?

我們按照乘以 2 取整數(shù)位的方法,把 0.1 表示為二進(jìn)制(我假設(shè)那些不會(huì)進(jìn)制轉(zhuǎn)換的同學(xué)已經(jīng)補(bǔ)習(xí)完了):

(1) 0.1 x 2 = 0.2  取整數(shù)位 0 得 0.0
(2) 0.2 x 2 = 0.4  取整數(shù)位 0 得 0.00
(3) 0.4 x 2 = 0.8  取整數(shù)位 0 得 0.000
(4) 0.8 x 2 = 1.6  取整數(shù)位 1 得 0.0001
(5) 0.6 x 2 = 0.2  取整數(shù)位 1 得 0.00011
(6) 0.2 x 2 = 0.4  取整數(shù)位 0 得 0.000110
(7) 0.4 x 2 = 0.8  取整數(shù)位 0 得 0.0001100
(8) 0.8 x 2 = 1.6  取整數(shù)位 1 得 0.00011001
(9) 0.6 x 2 = 1.2  取整數(shù)位 1 得 0.000110011
(n) ...

我們得到一個(gè)無限循環(huán)的二進(jìn)制小數(shù) 0.000110011...

我為什么要把這個(gè)計(jì)算過程這么詳細(xì)的寫出來呢?就是為了讓你看,多看幾遍,再多看幾遍,繼續(xù)看… 還沒看出來,好吧,把眼睛揉一下,我提示你,把第一行去掉,從 (2) 開始看,看到 (6),對(duì)比一下 (2) 和 (6)。 然后把前兩行去掉,從 (3) 開始看…

明白了吧,0.2、0.4、0.6、0.8 都不能精確的表示為二進(jìn)制小數(shù)。 難以置信,這可是所有的偶數(shù)??!那奇數(shù)呢? 答案就是:

0.1 到 0.9 的 9 個(gè)小數(shù)中,只有 0.5 可以用二進(jìn)制精確的表示。

如果把 0.0 再算上,那么就有兩個(gè)數(shù)可以精確表示,一個(gè)奇數(shù) 0.5,一個(gè)偶數(shù) 0.0。 為什么是兩個(gè)呢?因?yàn)橛?jì)算機(jī)二唄,其實(shí)計(jì)算機(jī)還真夠二的。

世界上有 10 種人,一種是懂二進(jìn)制的,一種是不懂二進(jìn)制的。

其實(shí)答案很顯然,我再領(lǐng)大家換個(gè)角度思考,0.5 就是一半的意思。 在十進(jìn)制中,進(jìn)制的基數(shù)是 10,而 5 正好是 10 的一半。 2 的一半是多少?當(dāng)然是 1 了。 所以,十進(jìn)制的 0.5 就是二進(jìn)制的 0.1。如果我用八進(jìn)制呢? 不用計(jì)算你就應(yīng)該立刻回答:0.4;轉(zhuǎn)換成十六進(jìn)制呢,當(dāng)然就是 0.8 了。

(0.5)10?= (0.1)2?= (0.4)8?= (0.8)16

如果你還想繼續(xù)思考,就又會(huì)發(fā)現(xiàn)一個(gè)有趣的事實(shí),我們稱之為 定理A。 我們上面的數(shù),都是小數(shù)點(diǎn)后面一位小數(shù),因此,在十進(jìn)制中,這樣的小數(shù)有 10 個(gè)(就是 0 到 9); 同理,在二進(jìn)制中,如果我們讓小數(shù)點(diǎn)后面有一位小數(shù),應(yīng)該有多少個(gè)呢?當(dāng)然是 2 個(gè)了(0 和 1)。

哇,好像發(fā)現(xiàn)了新大陸一樣,很興奮是吧。那我再給你一棒,其實(shí)定理A是錯(cuò)的。再重申一遍?盡信書,則不如無書。我寫博客的目的?不是把我的思想灌輸?shù)侥愕哪X子里,你應(yīng)該有自己的思想,自己的思考方式,當(dāng)我得出這個(gè)結(jié)論時(shí),你應(yīng)該立刻反駁我:“按照你的思路,如果是 16 進(jìn)制的話,應(yīng)該可以精確表示所有的 0.1 到 0.9 的數(shù)甚至還可以精確表示其它的 6 個(gè)數(shù)。而事實(shí)呢,16 進(jìn)制可以精確表示的數(shù) 和 2 進(jìn)制可以精確表示的數(shù)是一樣的,只能精確表示 0.5?!?/p>

那么到底怎么確定一個(gè)數(shù)能否精確表示呢?還是回到我們熟悉的十進(jìn)制分?jǐn)?shù)。

1/2、5/9、34/25 哪些可以寫成有限小數(shù)?把一個(gè)分?jǐn)?shù)化到最簡(分子分母無公約數(shù)),如果分母的因式分解只有 2 和 5,那么就可以寫成有限小數(shù),否則就是無限循環(huán)小數(shù)。為什么是 2 和 5 呢?因?yàn)樗麄兪?10 的因子 10 = 2 x 5。

二進(jìn)制和十六進(jìn)制呢?他們的因子只有 2,所以十六進(jìn)制只是二進(jìn)制的一種簡寫形式,它的精度和二進(jìn)制一樣。

如果一個(gè)十進(jìn)制數(shù)可以用二進(jìn)制精確表示,那么它的最后一位肯定是 5。

備注:這是個(gè)必要條件,而不是充分條件。一位熱心網(wǎng)友設(shè)計(jì)出了下面的解決精度的方案。我就不解釋了,同學(xué)們自己思考一下吧。

我有一個(gè)觀點(diǎn),針對(duì)小數(shù)精度不夠的問題(例如 0.1),軟件可以人為的在數(shù)據(jù)最后一位補(bǔ) 5, 也就是 0.15,這樣犧牲一位,但是可以保證數(shù)據(jù)精度,還原再把那個(gè)尾巴 5 去掉。

請同學(xué)們思考一下。

精度在哪兒丟失?

一位熱心網(wǎng)友?獨(dú)孤小敗?在 OSC 上回復(fù)了我上一篇文章,提出了一個(gè)疑問:

在 java 中計(jì)算 0.2 + 0.4 得到的結(jié)果是

// 代碼(a)
double d = 0.2 + 0.4;  // 結(jié)果是 0.6000000000000001

但是當(dāng)直接輸出 0.6 的時(shí)候,確實(shí)是 0.6

// 代碼(b)
double d = 0.6;  // 結(jié)果是 0.6

好像很矛盾。很顯然,通過代碼(b)可以知道,在 java 中,可以精確?顯示?0.6,哪怕 0.6 不能被精確表示,但至少能精確把 0.6 顯示出來,這不是和代碼(a)矛盾了嗎?

這又是一個(gè)?想當(dāng)然的錯(cuò)誤,在直觀上認(rèn)為 0.2 + 0.4 = 0.6 是必然成立的(在數(shù)學(xué)上確實(shí)如此),既然(a)的結(jié)果是 0.6,而且 java 可以精確輸出 0.6,那么代碼(a)的結(jié)果應(yīng)該輸出 0.6。

其實(shí)在計(jì)算機(jī)上 0.2 + 0.4 根本就不等于 0.6 (為什么?可以查看本系列『運(yùn)算符』),因?yàn)?0.2 和 0.4 都不能被精確表示。?浮點(diǎn)數(shù)的精度丟失在每一個(gè)表達(dá)式,而不僅僅是表達(dá)式的求值結(jié)果。

我們用數(shù)學(xué)中的概念類比一下,比如四舍五入,我們計(jì)算 1.6 + 2.8 保留整數(shù)。

1.6 + 2.8 = 4.4 

四舍五入得到 4。我們用另一種方法

先把 1.6 四舍五入為 2
再把 2.8 四舍五入為 3
最后求和 2 + 3 = 5

通過兩種運(yùn)算,我們得到了兩個(gè)結(jié)果 4 和 5。同理,在我們的浮點(diǎn)數(shù)運(yùn)算中,參與運(yùn)算的兩個(gè)數(shù) 0.2 和 0.4 精度已經(jīng)丟失了,所以他們求和的結(jié)果已經(jīng)不是 0.6 了。

后記

上面一直在討論小數(shù),整數(shù)呢?在博客園,一位童鞋為下面的代碼抓狂了:

JSON.parse('{"status":1,"id":9986705337161735,"name":"test"}').id; 

把這段代碼復(fù)制到 Chrome 的 Console 中,按回車, 詭異的問題出現(xiàn)了 9986705337161735 居然變成了 9986705337161736!原始數(shù)據(jù)加了 1。

9986705337161735
9986705337161736

一開始以為是溢出,換了個(gè)更大的數(shù):9986705337161738 發(fā)現(xiàn)不會(huì)出現(xiàn)這個(gè)問題。

但是 9986705337161739 輸出又變成了 9986705337161740!

9986705337161739
9986705337161740

測試幾次之后發(fā)現(xiàn)瀏覽器輸出數(shù)字的一個(gè)規(guī)律(justjavac注:其實(shí)這個(gè)規(guī)律是錯(cuò)誤的):

  1. 十位數(shù)為偶數(shù),個(gè)位數(shù)為奇數(shù)時(shí)會(huì)減 1,個(gè)位數(shù)為奇數(shù)時(shí)會(huì)加1
  2. 十位數(shù)為奇數(shù),個(gè)位數(shù)為奇數(shù)時(shí)會(huì)加 1,個(gè)位數(shù)為奇數(shù)時(shí)會(huì)減1

又多測了幾次,發(fā)現(xiàn)根本沒有規(guī)律,很混亂!!有時(shí)候是加,有時(shí)候是減??!

解析

這顯然不僅僅是丟失精度的問題,欲知后事如何…咳咳…靜待下一篇吧。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)