知多一點二進位制中的負數

sea_ljf發表於2019-02-10

hello~親愛的看官老爺們新年好~相信不少同學知道,如果要將一個數字轉換為它的相反數,在 Javascript 中,除了在它前面加個-號之外,還可以對該數字進行取反,之後再加 1。前者(本質是 0 減去對應的數字)可以得到相反數,完全符合我們的直覺,但為何取反加一也可以,這看起來不太科學,本文將帶你一探究竟~

友情提示,計算機科班的童鞋,對此應該是爛熟於心了,可能對你幫助有限。但不清楚其中細節的同學,希望本文能滿足你好奇心之餘,瞭解多一點二進位制的知識。以下是正文~

從減法開始

估計各位童鞋肯定聽過以下這條法則:

減去一個數,等於加上這個數的相反數。

同時也應該瞭解,為了區分正負數,二進位制中的最高位是符號位,其中正數的符號位是 0,而負數的符號位為 1。以 Java 的 byte 型別(8位,範圍是 -128 ~ 127,即 0000000011111111)為例,如果按照直覺,既然最高位為符號位,那麼 -1 應該表示為 10000001。想法很美好,現實卻很骨感。

思考以下問題,儘管負數不確定,但我們肯定正數 1 表示為 00000001,如果需要得到算式 1-1 的結果 ,按照上面的法則,可以將算式轉化為 1+(-1) 進行運算,但是 10000001 + 00000001,無論怎麼進行運算,似乎都很難得到 00000000 這個值吧?由此可見,計算機中儲存負數的值,並不是那麼簡單的~

撥動時鐘

負數的探索似乎陷入了死衚衕,那讓我們先把目光轉移一下,從螢幕轉到牆上的時鐘之上~假設這個鍾時針和分針可以分別進行調整,互不影響,且現在時鐘顯示的時間是 10:40,但對比北京時間,快了 10 分鐘,那麼我們改如何調整時鐘呢?簡單地做個算式:

    45
-   10
----------
    35
複製程式碼

得出的答案是 35,那麼將分針調整到 35 的位置即可,也不需要調整時針。很簡單對吧?那如果現在時間也是 10:40,但時間快了 15 分鐘,那該如何調整?你心中估計會想,那還不簡單,豎式一樣能算出來~但這裡加一個需求,不希望豎式中出現借位(也就是 運算 40-15 時,由於被減數個位不如減數個位大,需要被減數從十位中借 1 到個位之上),那該如何實現呢?

emmmmmmmm,似乎比較麻煩~借位是由於被減數的某一位不夠減數大而導致的,那如果被減數足夠大,似乎就能解決這問題。原式是 40-15,可以等價改寫為 40+(59-15)-59,答案是完全一致的。但這裡還是有問題,運算兩次之後,剩下的算式為 84-59,仍然要借位不是嗎?那我們再改寫一下:40+(59-15)+1-60。這樣的算式運算下來,不再需要借位了。換成時鐘操作,就是直接將分針調整到 25 即可。

看到這裡,估計你心中會多了一點明悟,但似乎其中還有一些說不過去的地方對吧?比如這個例子:現在的時間是 10:15,現在的時間比北京時間快了 40 分鐘,那該如何進行運算呢?按照上面的例子,可以依樣寫下這條算式 15+(59-40)+1-60,但問題來了,算到最後,是 35-60,還是要借位不是嗎?

是,但可以不是。-60 在時鐘上的本質是,將時針回撥一下。那麼 35-60,其實可以分解為兩個操作,時針撥動到 35 的位置,時針回撥到 9 的位置,現在的時間為 09:35,難道不是正確的答案嗎?

不那麼一樣的數軸

在恍然大悟前先停一下,還有一點點東西需要了解。相信數軸的概念銘刻在各位心中,它是完全符合直覺的,且數軸是無窮的,相信大家也都知道,一般印象中的數軸如下:

-60, -59, -58, -57···, -2, -1, 0, 1, 2···57, 58, 59

但如果數軸是有範圍的話,假設總共只有 120 個整數在數軸上,如果我們想消除負號,那麼用正數表示負數,也未嘗不可,比如以 59 為分界,大於 59 的數均為負數。即 -1 表示為 119,-40 表示為 80,-25 表示為 95 等:

60, 61, 62, 63···118, 119, 0, 1, 2···57, 58, 59

那麼之前時鐘的例子,是以 59 作為避免借位的被減數,但這是為了方便時鐘撥動,這裡我們再往前跨一步,剛才時鐘的運算:15-40 可以表示為 15+80,運算結果是 95,對錶查詢可得,95 的值代表的是 -25,運算正確!

你可能會吐槽減法是借位了,但主要是為了方便映照時鐘的例子,換成 999 作為避免借位的被減數,就是標準對 9 的補數。不妨在紙上畫一下,此時 -1 表示為 999,-40 表示為 959,-25 表示為 974,15-40015+959,運算結果是 974,也是完全對應上的。

重回二進位制

有了上面的鋪墊,二進位制的負數已經呼之欲出啦~符號位天然可以作為正負數的分割點。還是以 Java 的 byte 型別為例,8 位一共可以表示 256 個數字,由於 0 表示為 00000000,首位符號位也是 0,因而正數少一位,按照上一節的例子,推出當前序列為:

10000001, 10000002···11111101, 11111110, 11111111, 00000000, 00000001···01111101, 01111110, 01111111

二進位制數 十進位制數
10000000 -128
10000001 -127
10000002 -126
··· ···
11011000 -40
··· ···
11100111 -25
··· ···
00001111 15
··· ···
01111111 127

那麼還是這條算式:15-40,二進位制中即為 00001111+11011000,稍微數一下手指,得出答案是 11100111,對應的值為 -25!然而,這個表背起來是沒意義的,那該如何運算呢?思考一下,之前運算 15-40 時,我們轉換為 15+(59-40)+1-60,59 為足夠大的被減數,在 byte 型別中,最大的數不會超過 11111111(即 255),因而可以轉換為:

    11111111(255)
-   00101000(40)
+   00000001(1)
+   00001111(15)
-  100000000(256)
複製程式碼

二進位制其實十分有趣,先觀察首先需要運算的 11111111-00101000,結果是 11010111,也就是各位取反,下一步是加一,得到結果是 11011000,不就是表中 -40 對應的值了麼?所以該表的推導是有嚴格的數學意義的,並不是隨便編造一個表,到這裡,應該明白為何在計算機中,取某個數的相反數是取反再加一了吧~

算式最後一步是減去 256,計算機中這步都可以省下了,因為 256 已經超過 byte 的範圍,直接忽略。同理,如果計算如 -1+1 之類的算式時,會得到答案是 256 (即 100000000),但由於超過範圍,首位 1 直接被忽略不計,因而得出的答案是 0 。以上運算,符號位均參與運算,並不需要區別對待,這對計算機而言是非常友好的。

小結

為嚴謹起見,補充兩個條件:

  1. 運算數均為整數。
  2. 計算結果均不溢位。

以上就是全文的內容啦,二進位制相關的知識,其實是相當有意思的,也許瞭解它們並不會使我們的程式碼能力突飛猛進,但保持好奇心,不斷探索未知的領域,一定是一個良好的習慣~

感謝各位看官大人看到這裡,知易行難,希望本文對你有所幫助~謝謝!

參考資料

補碼

《編碼:隱匿在計算機軟硬體背後的語言》

程式是怎樣跑起來的

相關文章