位運算總結,我的世界裡只有 0 和 1
本文首發於公眾號「 MoTec 」,閱讀原文效果更佳。 >>> ofollow,noindex">傳送門
在上一篇我自己原創的文章「 N 種方式來訪問百度、Google 」中,我們談到了 IP 字串與不同進位制的數字間相互轉換,在程式碼中涉及了位運算、進位制轉換,雖然我在公眾號文章中沒對程式碼進行詳細地解釋,至於為什麼,相信看了上篇文章的 Friend 都知道。但是我在部落格上,就配合圖文進行了詳解,截圖給大家可以圍觀一下

博文地址是 https://blog.csdn.net/MOESECSDN/article/details/84256572,有可能失效。
此文也是我對程式碼中的位運算思考後的一些總結,部分內容在其他部落格中是找不到的,也肯定沒有那麼詳細,因此分享出來,希望對大家有所幫助。如果覺得文章不錯,不妨點個贊,轉發給有需要的 Friend。
鋪墊
我們知道,目前的計算機最終只認識 0 和 1 這兩個數字,我們寫的所有程式碼、指令最終都會變成以 0 和 1 組成的編碼執行的,而這樣的編碼就叫做二進位制。
至於為什麼是 0 和 1 呢?我簡單、非官方地解釋一下,因為計算機是由無數個邏輯電路組成的,而電路的邏輯只有 0 和 1 兩個狀態,0 和 1 並不是簡單數字意義上的 0 和 1,它們表示兩種不同的狀態,0 表示低電平,1 表示高電平。要控制電路來表達某種意思,就只能控制不同電路的不同狀態即根據 0 和 1 的有限位數和組合來表達。

因此像我這些從事計算機相關學習或者工作的人就自詡「我的世界裡只有 0 和 1」。
而今天我要說的「位運算」,就是直接對這些二進位制位進行的一些操作,當然也只是在數值方面。常用的二進位制位操作有,~(取反)、^(異或)、>>、<<、&(與)、|(或),在 Java 中還有 >>>,下面是一些簡單的規則

另外補充一下,1 & X = X,0 & X = 0,1 | X = 1,0 | X = X。
最重要的是,在計算機系統中,數值一律用補碼來表示(儲存)。 因為使用補碼可以將符號位和其它位統一處理,同時,減法也可按加法來處理。
其中,如果是我們要人為計算的話(一些面試題,很噁心),碰到負數一定要一萬個小心,負數在記憶體中儲存的是它的補碼,而它是原碼取反加 1 而不是像正數那樣,補碼和原碼一樣,另外取反操作也需要特別小心。
~(取反)
0 和 1 全部取反,0 變為 1,1 變為 0。即 ~ 0 = 1,~ 1 = 0。一定要特別要注意的是,這裡的 0 和 1 是二進位制位中的,它是一個位,跟我們常用的十進位制中的 0 和 1 區別非常大!舉個例子,順便說一下正數的取反運算,你或許會清楚怎麼回事。你覺得下面的程式碼會輸出什麼?
class Test { public static void main(String[] args) { System.out.println(~1); } }
會是 0 嗎?大錯特錯!千萬別以為這是前面說的~ 1 = 0,答案是 -2

為什麼是 -2 呢?程式碼裡的 1 跟前面規則表格中的 1 區別很大,表格中的 1 是具體到某一位,真正的位操作,而程式碼裡 1 是十進位制中的 1,它是 int 型別,在 Java 中,它要用 4 個位元組即 32 位來表示,即

那它取反怎麼成 -2 了呢?首先它是正數,它的補碼和原碼是一樣的,也就是
00000000 00000000 00000000 00000001
特別提醒的是,上面的是 1 的補碼,取反之後是
11111111 11111111 11111111 11111110
注意最高位,也就是我們說的符號位,它也會被取反,0 變1,竟成了負數!同時一定要知道它是補碼,要轉換成原碼的話,先 -1 再取反,因為負數的補碼是由原碼取反後再 +1,現在是逆過程。

注意在這次取反過程中,符號位是不用取反的,但前面 ~ 取反操作是要取反的,這也是我們很容易錯的地方。
再來看看負數 -5 的 ~ 取反操作
class Test { public static void main(String[] args) { System.out.println(~-5); } }
你可以先動手試試,看看結果是不是 4

為了方便,我這裡就不再以 32 位來做演示,而是隻用 8 位,後面有些例子也是如此

小結,取反操作是不管符號位的,總之都取反,0 變 1,1 變 0,而在原碼和補碼間轉換時,雖然也有個取反過程,但是符號位是不變的,這也是我們經常會混淆的,是坑。
另外,對所有位操作,實際上都是對它的補碼操作,這個適用於任何位操作。對於正數,巧就巧在補碼和原碼一樣,而負數的補碼是原碼的取反加 1,所以我們也會混淆。
| 與、& 或
對於 ^ 異或運算我在這裡就不多說了。就說說我在 | 或運算的小結,大家也可以類推到 & 與運算,然後,再說說 Java 中 >> 和 >>> ,就結束。碼字好累,原創不易,多多點贊支援,謝謝。
先給個小題,16 | 15 = ?
我先不直接揭曉,一起來看看計算過程,還是以 8 位來做演示

最終結果是 31,不知大家有沒有覺得蹊蹺,16 | 15 = 31 = 16 + 15,在這裡,| 或運算相當於加法運算。
其實還可以看看其他例子,32 | 9 = 41 = 32 + 9

為什麼會這樣呢?因為 1 | X = 1,0 | X = X ,我們再認真看位運算的過程

我們以第一個加數為基數,末尾除了右起第 6 位,都是 0 ,而第二個加數又小於它,一經過 | 或運算, 0 | X = X ,其實也是將兩位數加一起。
所以這裡有個小結論,2^N 與一個小於它的數做 | 或運算,其實就是它們兩個數之和。知道這個結論,我們以後做題時運算效率就更高一些。就像數字轉 IP 的演算法就把這個用到極致,它還結合 << 。
public long ipToLong(String ipStr) { long result = 0; String[] ipAddressInArray = ipStr.split("\\."); for (int i = 3; i >= 0; i--) { long ip = Long.parseLong(ipAddressInArray[3 - i]); // 等同 A * 256^3 + B * 256^2 + C * 256^1 + D * 256^0,運用位移、或 位運算更高效 result |= ip << (i * 8); } return result; }
更深層次的,在非負兩數或運算中,只要兩數換成二進位制數時,對應的位不是 1 | 1,或運算結果都與加法運算結果一致,我稱它為或運算中的非雙一現象。

上面的程式碼就可以很好的詮釋,其中 0b 表示二進位制數的寫法,就好像 0x 表示十六進位制一樣道理,數值我是隨便給的,不是我故意,大家回去可以試試。通常我們會感覺沒什麼卵用,還不如前面的小結論來得實在點,其實不然,如果知道這些現象且用得非常 6,在加密、演算法效率方面用處是非常大的,我就因為欠缺這個而丟失一份很好的工作。
>> 和 >>>
先解釋符號及運算規則,>>,帶符號右移,正數右移高位補 0,負數右移高位補 1;
4 >> 1 = 2

-4 >> 1 = -2

>>>,無符號右移。無論是正數還是負數,高位通通補 0 。
4 >>> 1 = 2

-4 >>> 1 = 2147483646

程式碼執行結果驗證

小結一下,對於正數而言,>> 和 >>> 沒區別。對於負數,>> 將二進位制高位用 1 補上,而 >>> 將二進位制高位用 0 補上,區別就很大。
另外,位運算可以幫我們高效地完成很多事情,例如求平均數、判斷奇偶、不借助第三方交換兩個數 ……,簡單瞭解後,我的世界觀都重造了,計算機的世界裡好神奇,有興趣可以查閱相關部落格和書籍。
推薦閱讀
本文章首發於公眾號「MoTec」,公眾號定期特別推送 Java、Python 乾貨、一起討論技術、思維認知、投資理財;共享網路資源,分享個人所知一切,一起成長、To Be Better。
