概述#
Bit manipulation 在各種面試題中都會出現。有時題目明確要求使用位元操作,有時則是優化程式碼的有效手段。你必須能夠用手算和用程式碼操作位元,且要小心不要犯細節錯誤。
Bit Manipulation By Hand#
練習用手算以下位元運算是打好基礎的關鍵。^ 表示 XOR,~ 表示 NOT(取反)。
| 運算式 | 說明 |
|---|---|
0110 + 0010 | 二進位加法 |
0011 * 0101 | 二進位乘法 |
0110 + 0110 | 等同 0110 * 2,即左移 1 位 |
0100 * 0011 | 0100 = 4,乘以 4 等同左移 2 位,得 1100 |
a ^ (~a) | 任何位元與其取反值做 XOR 必得 1,結果為全 1 序列 |
~0 << 2 | ~0 是全 1 序列,左移 2 位後末兩位變 0 |
不確定時,先用十進位思考同樣的運算,再套用到二進位上。
Bit Facts and Tricks#
以下表達式在位元操作中非常實用。不要死背,要理解為什麼它們成立:
x ^ 0s = x x & 0s = 0 x | 0s = x
x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x這些運算都是 bit-by-bit 獨立進行的,每個位元互不影響。
Two’s Complement and Negative Numbers#
電腦通常使用 Two’s complement(二補數) 來儲存整數:
- 正數以本身表示,最高位(sign bit)為 0
- 負數以其絕對值的二補數表示,最高位為 1
N 位元整數中,-K 的二進位表示為 concat(1, 2^(N-1) - K)。
另一種理解方式:將正數的所有位元取反後加 1,再加上符號位 1。
範例:4 位元整數 -3
- 3 的二進位為
011 - 取反得
100,加 1 得101 - 加上符號位得
1101
4 位元整數的二補數對照表:
| 正數 | 二進位 | 負數 | 二進位 |
|---|---|---|---|
| 7 | 0 111 | -1 | 1 111 |
| 6 | 0 110 | -2 | 1 110 |
| 5 | 0 101 | -3 | 1 101 |
| 4 | 0 100 | -4 | 1 100 |
| 3 | 0 011 | -5 | 1 011 |
| 2 | 0 010 | -6 | 1 010 |
| 1 | 0 001 | -7 | 1 001 |
| 0 | 0 000 |
觀察左右兩側的絕對值之和永遠等於 2^3 = 8,且除符號位外二進位值完全相同。
Arithmetic vs. Logical Right Shift#
右移有兩種類型:
Logical Right Shift(邏輯右移)>>>#
- 將位元向右移動,最高位補 0
- 視覺上就是單純的位元右移
- 對 8 位元的
-75(10110101)邏輯右移 1 位,得01011010= 90
Arithmetic Right Shift(算術右移)>>#
- 將位元向右移動,最高位補符號位的值(正數補 0,負數補 1)
- 效果相當於除以 2(取整)
- 對
-75(10110101)算術右移 1 位,得11011010= -38
int repeatedArithmeticShift(int x, int count) {
for (int i = 0; i < count; i++) {
x >>= 1; // Arithmetic shift by 1
}
return x;
}
int repeatedLogicalShift(int x, int count) {
for (int i = 0; i < count; i++) {
x >>>= 1; // Logical shift by 1
}
return x;
}對
x = -93242,count = 40執行:
- 邏輯右移:結果為 0(持續將 0 移入最高位)
- 算術右移:結果為 -1(持續將 1 移入最高位,全 1 序列的有號整數為 -1)
Common Bit Tasks: Getting and Setting#
以下操作非常重要,但不要死背。要理解如何實作,這樣才能舉一反三,解決各種位元問題。
Get Bit#
將 1 左移 i 位,得到形如 00010000 的 mask。與 num 做 AND,清除所有其他位元,只保留第 i 位。比較結果是否為 0。
boolean getBit(int num, int i) {
return ((num & (1 << i)) != 0);
}Set Bit#
將 1 左移 i 位,得到形如 00010000 的 mask。與 num 做 OR,只會改變第 i 位,其餘位不受影響。
int setBit(int num, int i) {
return num | (1 << i);
}Clear Bit#
與 setBit 幾乎相反。先建立 00010000,取反得 11101111,再與 num 做 AND,清除第 i 位。
int clearBit(int num, int i) {
int mask = ~(1 << i);
return num & mask;
}清除從 MSB 到第 i 位(含)的所有位元:
int clearBitsMSBthroughI(int num, int i) {
int mask = (1 << i) - 1;
return num & mask;
}清除從第 i 位到第 0 位(含)的所有位元:
int clearBitsIthroughO(int num, int i) {
int mask = (-1 << (i + 1));
return num & mask;
}Update Bit#
先用 mask 11101111 清除第 i 位,再將目標值 v 左移 i 位,用 OR 寫入。
int updateBit(int num, int i, boolean bitIs1) {
int value = bitIs1 ? 1 : 0;
int mask = ~(1 << i);
return (num & mask) | (value << i);
}本章面試題列表#
| 題號 | 題目 | 頁碼 |
|---|---|---|
| 5.1 | Insertion(位元插入) | pg 276 |
| 5.2 | Binary to String(實數轉二進位字串) | pg 277 |
| 5.3 | Flip Bit to Win(翻轉一個位元使連續 1 最長) | pg 278 |
| 5.4 | Next Number(找相同 1 的個數的前後整數) | pg 280 |
| 5.5 | Debugger(解釋 (n & (n-1)) == 0 的意義) | pg 285 |
| 5.6 | Conversion(計算 A 轉為 B 需翻轉幾個位元) | pg 286 |
| 5.7 | Pairwise Swap(交換奇偶位元) | pg 286 |
| 5.8 | Draw Line(在單色屏幕上畫水平線) | pg 287 |