什麼, 0.3 - 0.2 ≠ 0.1 ?

標籤: 公衆號文章算法


慘痛的歷史教訓

記得還在上學那會兒,給咱們上《運籌學》的老師留了一個課程實驗,就是讓咱們每一個人都去實現一個書中所講的算法。因爲當時徹底沒有什麼分層、模塊化的啥概念,寫代碼就是一股腦往裏塞邏輯,寫出來的代碼用一坨形容徹底不爲過。編程

當我實現完算法以後,開始弄幾個值做爲輸入進行測試,發現有的值能夠測試成功,有的卻不行,怎麼辦呢?調試唄,面對着那麼一大坨亂糟糟的代碼,調試簡直是災難,好像花了整整一下午加晚上的時間去調試,調試的我兩眼冒金星,脖子轉不動,真是:寫代碼一時爽,調試火葬場bash

最後我居然發現了一個神奇的現象(也是後來一直銘記的教訓):編程語言

0.2 - 0.1 == 0.1 這個表達式的結果爲true,可是0.3 - 0.2 == 0.1的這個表達式的結果居然爲false,我滴個乖乖,真不敢相信本身的眼睛。後來查了查書說是浮點數表示的數字並非精確的,到底怎麼個不精確法,本篇文章就來嘮叨嘮叨~模塊化

浮點數究竟是怎麼表示的?

浮點數實際上是用來表示小數的,咱們平時用的十進制小數也能夠被轉換成二進制後被計算機存儲。好比9.875,這個小數能夠被表示成這樣:測試

9.875 = 8 + 1 + 0.5 + 0.25 + 0.125 
      = 1 × 2³ + 1 × 2⁰ + 1 × 2⁻¹ + 1 × 2⁻² + 1 × 2⁻³ 
複製代碼

也就是說,若是十進制小數9.875轉換成二進制小數的話就是:1001.111。爲了在計算機裏存儲這種二進制小數,咱們統一把它們表示成a × 2ⁿ的科學計數法的形式,其中1≤|a|<2,好比1001.111能夠被表示成1.001111 × 2³。咱們把小數點以後的001111稱爲尾數,把中的3稱爲指數,又由於一個數字有正負之分,因此爲了表示一個小數,只須要下邊這幾部分就能夠了:spa

  • 符號部分。設計

  • 尾數部分。調試

  • 指數部分。code

根據表示尾數和指數所使用的存儲空間大小不一樣,浮點數又被具體細分爲:

  • 單精度浮點數(通常編程語言中的float類型):

    單精度浮點數總共佔用4個字節:

    • 使用1個比特位表示符號部分,值爲0時表示正數,值爲1時表示負數。

    • 使用8個比特位表示指數部分。

    • 使用23個比特位表示尾數部分。

    畫個示意圖就是這樣:

    image_1df37645t1v4mml1eo3t3u1v04m.png-78.2kB

  • 雙精度浮點數(通常編程語言中的double類型):

    雙精度浮點數總共佔用8個字節:

    • 使用1個比特位表示符號部分。

    • 使用11個比特位表示指數部分。

    • 使用52個比特位表示尾數部分。

    示意圖就不畫了。

對於某個使用科學技術法表示的二進制小數,直接把指數對應的數字和尾數對應的數字填到對應的位置就完了麼?好比對於二進制小數1.001111 × 2³(也就是十進制的9.875),若是用單精度浮點數表示的話應該是這樣麼(爲了清楚的表示各個部分,咱們用空格把各個部分分隔開,其實是沒有間隔的):

0 00000011 00111100000000000000000
複製代碼

其中:

  • 第一個比特位0表示這是一個正數。

  • 00000011表示指數3

  • 00111100000000000000000表示尾數001111

哈哈,事實並無這麼簡單,設計浮點數的大叔出於某種目的而把事情搞的稍微有些複雜(這個複雜是針對咱們人類說的)。他們把指數部分看成一個無符號數,這個無符號數的字面量並非真實的指數值,而是通過一些曲折的計算手段才能獲得最後的指數值。他們把浮點數的存儲方式分爲了3種狀況討論:

  • 當指數部分的比特位既不全爲0(數值0),也不全爲1(單精度時爲255,雙精度時爲2047)時:

    這時真實的指數值等於指數部分的字面量減去一個偏置值,這個所謂的偏置值在單精度浮點數中是127,在雙精度浮點數中是1023

    比方說咱們想使用單精度浮點數來存儲二進制小數1.001111 × 2³,它的指數值爲3,咱們須要讓指數部分的字面量減去127的值爲3,因此指數部分的字面量就是130,用二進制表示就是:10000010。尾數部分是001111,因此表示二進制小數1.001111 × 2³的真正的單精度浮點數形式就是:

    0 10000010 00111100000000000000000
    複製代碼

    小貼士: 這是爲毛呀?爲啥要在字面量的基礎上減去一個所謂的偏置值。其實設計浮點數的大叔是這樣考慮的,對於表示指數的一堆比特位來講,它們總共能表示的數字個數是肯定的,比方說單精度浮點數的指數部分佔用8個字節,那麼就總共能表示2⁸個數字,也就是256個數字,不過比特位全爲0和全爲1時有特殊用途,因此指數部分最多能表示254個數字,能表示的數字範圍就是-126~127。他們想讓做爲無符號數字面量最小的那個二進制數,也就是00000001來表示這254個數字中最小的那個,也就是-126,而後隨着字面量的增大,表示的真實指數值也逐漸增大,直到最大的字面量11111110來表示真實的指數值127。這樣只須要在字面量的基礎上減去127就能達到這個效果。相似的,對於雙精度浮點數來講,就得在指數部分的無符號字面量的基礎上減去1023才能獲得真實的指數值。看到了吧,設計浮點數的大叔只是想讓字面量越大,真正的指數值越大這個目的才引入了偏置值這個怪怪的概念。

  • 當指數部分的比特位都爲0時:

    這是一種特殊的狀況,此時的指數並不表明0,而表明1減去偏置值,對於單精度浮點數來講,也就是指數表明1 - 127 = -126

    不過上一種狀況中不是已經表示了指數值爲-126的狀況了麼,爲啥還要把這這個指數值爲-126的狀況單獨提出來呢?這個還得從咱們表示1.001111 × 2³這個二進制小數的例子提及,別忘了咱們在浮點數的尾數部分存儲的是001111,也就是自動忽略了小數點左邊的那個1,這樣就節省了1個比特位。不過在指數部分全爲0的狀況下,尾數部分是不包含小數左邊的那個1的,比方說有一個單精度浮點數:

    0 00000000 01010000000000000000000
    複製代碼

    這個浮點數表示的二進制小數就是:0.0101 × 2-¹²⁶

    能夠看到,當單精度浮點數的尾數部分的比特位都爲0時,表示的都是比1 × 2-¹²⁶小的數字,也就是接近0的那部分數字。

    當浮點數尾數部分的比特位都是0時,能夠表示數值0.0,不過因爲符號位(就是第一個二進制位)的存在,因此有+0.0-0.0之分。

  • 當指數部分的比特位都爲1時:

    此時能夠再細分爲兩種狀況:

    • 當尾數部分的比特位都是0時:

      此時表示一個無窮大的值,當符號位爲0時,表示正無窮大,當符號位爲1時,表示負無窮大。

    • 當尾數部分的比特位不都爲0時:

      此時表示一個NaN值,NaN的全稱就是Not a Number,也就是不表明一個數,這在某些狀況下是有用的。

至此,浮點數存儲方式的三種狀況就嘮叨完了。不知道你們有沒有發現一個規律,就是在不考慮符號位時,假設咱們把浮點數的其他比特位當作一個無符號數,那麼這個無符號數的值越大,它所表示的浮點數值也越大,這樣在作浮點數比較大小操做時便十分簡單。

再看浮點數運算

看完浮點數的存儲格式以後,咱們再回過頭看一下最初提出的0.2 - 0.1 == 0.1值爲true,而0.3 - 0.2 == 0.1值爲false的狀況。咱們以單精度浮點數爲例,看一下這些表達式裏涉及到的這些數字該如何表示:

  • 0.1

    十進制小數0.1沒法轉成尾數在23位之內的二進制小數,因此只能通過舍入,獲得近似值:

    1.10011001100110011001101 × 2⁻⁴
    複製代碼

    指數值是-4,根據咱們上邊所述的第一種狀況,指數部分的字面量就是123,表示成二進制小數就是01111011,因此咱們能夠獲得十進制小數0.1對應的單精度浮點數就是:

    0 01111011 10011001100110011001101
    複製代碼
  • 0.2

    它的二進制小數近似值就是:

    1.10011001100110011001101 × 2⁻³
    複製代碼

    同理,能夠獲得以下單精度浮點數

    0 01111100 10011001100110011001101
    複製代碼
  • 0.3

    它的二進制小數近似值就是:

    1.00110011001100110011010 × 2⁻²
    複製代碼

    同理,能夠獲得以下單精度浮點數:

    0 01111101 00110011001100110011010
    複製代碼

那麼:

  • 計算0.2 - 0.1的值

    就至關於計算:

    1.10011001100110011001101 × 2⁻³ - 1.10011001100110011001101 × 2⁻⁴
    複製代碼

    獲得的結果就是:

    1.10011001100110011001101 × 2⁻⁴
    複製代碼

    而這個值正好是十進制小數0.1的二進制小數表示形式,因此0.2 - 0.1 == 0.1這個表達式的結果就爲true

  • 計算0.3 - 0.2的值

    就至關於計算:

    1.00110011001100110011010 × 2⁻² - 1.10011001100110011001101 × 2⁻³
    複製代碼

    獲得的結果就是:

    1.10011001100110011001110 × 2⁻⁴
    複製代碼

    而這個值並非十進制小數0.1的二進制小數表示形式,因此0.3 - 0.2 == 0.1這個表達式的結果就爲false

    這種計算結果的差別主要是由於十進制小數轉換爲二進制小數須要很是多的比特位,甚至轉爲的二進制小數是無限小數,而使用浮點數來表示二進制小數時使用的存儲空間是有限的,必須進行必定程度的舍入操做,這樣表示的二進制小數就不精確,採用浮點數進行運算的結果就不精確,你們在平常使用浮點數的過程當中要多加註意。

題外話

寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號「咱們都是小青蛙」,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:

相關文章
相關標籤/搜索