天天看點

0.28+0.34=? 一個簡單小數加法引發的思考

  0.28+0.34=?

  我相信這個簡單的加法,誰都會,肯定等于0.62嘛。

  這是兩個特别簡單的加法,那如果我在其整數位置上加上其他的數字,或者多加幾個和項,你是否還能快速算過來?

  我想這時候,我們又得借助電腦了!而這,有時可能就是電腦!尤其是如果咱們借助簡單程式語言來算的時候,嘿嘿,可能就不是那麼回事了~

不信你看,用javascript算的結果:

0.28+0.34=? 一個簡單小數加法引發的思考
用python算的結果:
0.28+0.34=? 一個簡單小數加法引發的思考

當然了,我嘗試着用其他語言來試一下,結果好像并不都是這樣。

其中,java隻會在類型轉換的時候出現奇怪的值:(當然這在我們寫代碼時往往很容易這麼幹)

0.28+0.34=? 一個簡單小數加法引發的思考

好了,前言就到此為止!咱們是要來看一下,為什麼 1+1不等于2 ?

其實這是由浮點數在計算機中的存儲方式決定的,因為計算機隻認識0101,是以小數點的儲存就需要使用另外的算法來轉換了,大概如下:(以下内容參考網絡知識庫)

計算機中是用有限的連續位元組儲存浮點數的。 儲存這些浮點數當然必須有特定的格式, C/C++中的浮點數類型 float 和 double 采納了 IEEE 754 标準中所定義的單精度 32 位浮點數和雙精度 64 位浮點數的格式。 在 IEEE 标準中,浮點數是将特定長度的連續位元組的所有二進制位分割為特定寬度的符号域,指數域和尾數域三個域, 其中儲存的值分别用于表示給定二進制浮點數中的符号,指數和尾數。 這樣,通過尾數和可以調節的指數(是以稱為"浮點")就可以表達給定的數值了。

32位浮點數存儲結構如下:

0.28+0.34=? 一個簡單小數加法引發的思考
三個主要成分是:
  • Sign(1bit):表示浮點數是正數還是負數。0表示正數,1表示負數
  • Exponent(8bits):指數部分。類似于科學技術法中的M*10^N中的N,隻不過這裡是以2為底數而不是10。需要注意的是,這部分中是以2^7-1即127,也即01111111代表2^0,轉換時需要根據127作偏移調整。
  • Mantissa(23bits):基數部分。浮點數具體數值的實際表示。

根據國際标準IEEE 754,任意一個二進制浮點數V可以表示成下面的形式:

    V = (-1)^s×M×2^E

  (1)(-1)^s表示符号位,當s=0,V為正數;當s=1,V為負數。

  (2)M表示有效數字,大于等于1,小于2,但整數部分的1可以省略。

  (3)2^E表示指數位。

比如:

對于十進制的5.25對應的二進制為:101.01,相當于:1.0101*2^2。是以,S為0,M為1.0101,E為2。

而-5.25=-101.01=-1.0101*2^2.。是以S為1,M為1.0101,E為2。

浮點數是如何存儲的,來看另一篇文章的簡單解說(https://www.cnblogs.com/yiyide266/p/7987037.html):

Step 1 改寫整數部分

以數值5.2為例。先不考慮指數部分,我們先單純的将十進制數改寫成二進制。

整數部分很簡單,5.即101.。

Step 2 改寫小數部分

小數部分我們相當于拆成是2^-1一直到2^-N的和。例如:

0.2 = 0.125+0.0625+0.007825+0.00390625即2^-3+2^-4+2^-7+2^-8….,也即.00110011001100110011。

或者換個更傻瓜的方式去解讀十進制對二進制小數的改寫轉換,通常十進制的0.5也(也就是分數1/2),相當于二進制的0.1(同等于分數1/2),

我們可以把十進制的小數部分乘以2,取整數部分作為二進制的一位,剩餘小數繼續乘以2,直至不存在剩餘小數為止。

例如0.2可以轉換為:

0.2 x 2 = 0.4     0

0.4 x 2 = 0.8     0

0.8 x 2 = 1.6     1

0.6 x 2 = 1.2     1

.......

即:.0011001.......(它是一個4862的無限循環的二進制數,明白為什麼十進制小數轉換成二進制小數的時候為什麼會出現精度損失的情況了嗎)

Step 3 規格化

現在我們已經有了這麼一串二進制101.00110011001100110011。然後我們要将它規格化,也叫Normalize。其實原理很簡單就是保證小數點前隻有一個bit。于是我們就得到了以下表示:1.0100110011001100110011 * 2^2。到此為止我們已經把改寫工作完成,接下來就是要把bit填充到三個組成部分中去了。

Step 4 填充

指數部分(Exponent):之前說過需要以127作為偏移量調整。是以2的2次方,指數部分偏移成2+127即129,表示成10000001填入。

整數部分(Mantissa):除了簡單的填入外,需要特别解釋的地方是1.010011中的整數部分1在填充時被舍去了。因為規格化後的數值整部部分總是為1。那大家可能有疑問了,省略整數部分後豈不是1.010011和0.010011就混淆了麼?其實并不會,如果你仔細看下後者:會發現他并不是一個規格化的二進制,可以改寫成1.0011 * 2^-2。是以省略小數點前的一個bit不會造成任何兩個浮點數的混淆。

好了,看完上面的浮點數的存儲原理後,是時候來解答,為什麼計算機會算錯的問題了!

  1. 遇到小數點後數字轉換為實際存儲結構時,有的轉換是一個死循環,即不可能得到一個精确的值,而這個不精确的值再與其他資料做運算時,得到的結果自然也就可能存在差距了。至于有時候能得到準确的數值,有時候卻得不到準備的值,則是和逆轉換相關了(即記憶體結構轉換為可視的十進制資料)!

  2. 另一個存在誤差的原因,則是因為在計算過程中進行了資料類型的轉換,因為原資料本來就不是精确的值,是以在進行類型轉換後,就不會得到和原始值直接轉化的值的相同結果了。

是以,咱們在做需要高精度的計算場合時,使用計算機語言自帶的存儲結構可能會不滿足咱們的需求,當然這也很容易辦到,一般也會有第三方的解決方案,即換一種存儲結構就可能能解決這種問題了。

  如 java 中,使用 BigDecimal 來解決需要高精度運算的場景。(BigDecimal的解決方案就是,不使用二進制,而是使用十進制(BigInteger)+小數點位置(scale)來表示小數);BigDecimal應使用string構造更為準确,否則會在第一步轉換時出現精度丢失!

最後,附幾個加法結果以供參觀:

>> 57168.619999999995-11087.28
46081.34
>> 2412.02+11087.64+8338.28+5580.0
27417.940000000002
>> 0.28+0.34
0.6200000000000001
>> 2.28+2.34
4.619999999999999
>> 33.28+3.34
36.620000000000005
>> 3.28+3.34
6.619999999999999
>> 4.28+4.34
8.620000000000001
>> 5.28+5.34
10.620000000000001
>> 8.28+8.34
16.619999999999997
>> 33.28+9.34
42.620000000000005      

不要害怕今日的苦,你要相信明天,更苦!

繼續閱讀