編碼 · 演算法
位元運算教學:運算子、技巧與範例
簡答
位元運算指的是使用位元運算子,直接操作整數的二進位位元(bit):AND(&)、OR(|)、XOR(^)、NOT(~)以及位移 << 和 >>。有了它們,你可以測試、設定、清除與切換單一位元,把一組小整數存進一個整數(位元遮罩),並解決像是計算設為 1 的位元數,或在額外常數空間內找出唯一未成對的數字之類的問題。對於固定大小的整數,每個運算都只需要一條快速的 CPU 指令。
位元運算解決什麼問題
整數是一排位元,許多問題其實就是在處理這些位元:像 rwx 的檔案權限、功能旗標、網路遮罩、像素顏色、精簡的選項集合、檢查碼與雜湊。位元運算子讓你一次運算就能讀取和修改許多位元,既節省記憶體,也非常快。
面試題也常用位元技巧,因為它們能得出優雅的 O(1) 空間解法,例如在其他數字都出現兩次時,找出唯一只出現一次的數字。
運算子
範例使用 a = 12(二進位 1100)和 b = 10(二進位 1010)。
| 運算子 | 名稱 | 每個位元的規則 | 範例 | 結果 |
|---|---|---|---|---|
a & b | AND(且) | 兩個位元都是 1 時為 1 | 1100 & 1010 | 1000 = 8 |
a | b | OR(或) | 任一位元是 1 時為 1 | 1100 | 1010 | 1110 = 14 |
a ^ b | XOR(互斥或) | 兩個位元不同時為 1 | 1100 ^ 1010 | 0110 = 6 |
~a | NOT(反相) | 翻轉每一個位元 | ~12 | -13(二補數) |
a << n | 左移 | 位元向左移動,右側補 0 | 12 << 1 | 24(乘以 2ⁿ) |
a >> n | 右移 | 位元向右移動 | 12 >> 2 | 3(除以 2ⁿ 並向下取整) |
a, b = 12, 10
print(bin(a), bin(b)) # 0b1100 0b1010
print(a & b, a | b, a ^ b) # 8 14 6
print(~a, a << 1, a >> 2) # -13 24 3
print(f"{a ^ b:04b}") # 0110
print(int("1010", 2)) # 10單一位元的操作
位元位置從最右邊(最低有效位元)的 0 開始算。遮罩 1 << i 只有第 i 個位元為 1。
| 工作 | 運算式 |
|---|---|
| 測試第 i 個位元 | (x >> i) & 1 或 x & (1 << i) != 0 |
| 設定第 i 個位元 | x | (1 << i) |
| 清除第 i 個位元 | x & ~(1 << i) |
| 切換第 i 個位元 | x ^ (1 << i) |
| 只保留最低位的 1 | x & -x |
| 清除最低位的 1 | x & (x - 1) |
| x 是否為 2 的冪?(x > 0) | x & (x - 1) == 0 |
範例:計算設為 1 的位元數
Brian Kernighan 的方法會不斷清除最低位的 1,直到沒有為止,所以迴圈次數等於 1 的個數,而不是位元位置的個數。以 x = 44(二進位 101100)追蹤:
| 步驟 | x(二進位) | x - 1(二進位) | x & (x - 1) | 計數 |
|---|---|---|---|---|
| 1 | 101100 | 101011 | 101000 | 1 |
| 2 | 101000 | 100111 | 100000 | 2 |
| 3 | 100000 | 011111 | 000000 | 3 |
def count_bits(x):
count = 0
while x:
x &= x - 1 # drop the lowest set bit
count += 1
return count
print(count_bits(44)) # 3
print((44).bit_count()) # 3
print((44).bit_length()) # 6int.bit_count() 從 Python 3.10 起可用。int.bit_length() 會回傳以二進位表示該數字所需的位元數(不含正負號)。
Python 中的經典技巧
from functools import reduce
from operator import xor
# Every number appears twice except one: pairs cancel out with XOR.
print(reduce(xor, [4, 1, 2, 1, 2])) # 4
# Swap two integers without a temporary variable (for illustration only).
x, y = 5, 9
x ^= y; y ^= x; x ^= y
print(x, y) # 9 5
# Bitmask as a set: enumerate all subsets of 3 items.
items = ["a", "b", "c"]
for mask in range(1 << len(items)):
print(mask, [items[i] for i in range(len(items)) if mask >> i & 1])
# 0 []
# 1 ['a']
# 2 ['b']
# 3 ['a', 'b']
# 4 ['c']
# 5 ['a', 'c']
# 6 ['b', 'c']
# 7 ['a', 'b', 'c']遮罩 0 是空集合,5(二進位 101)是 ['a', 'c'],7 則是全部三個項目。位元遮罩動態規劃用的是同樣的概念,以遮罩作為狀態,適用於最多約 20 個項目的問題,例如小規模輸入的旅行推銷員問題。
另外兩個經典用法:x & 1 對奇數為 1;將 0 到 n 的所有數字與串列中的所有值一起做 XOR,就能找出唯一缺少的數字。
時間與空間複雜度
| 運算 | 時間 | 說明 |
|---|---|---|
機器字長整數的 &、|、^、~ 與位移 | O(1) | 在 C、Java、Go 或 Rust 中只需一條 CPU 指令。 |
對 Python int 做相同運算 | O(位元數) | Python 整數的大小沒有上限;小的數值仍然非常快。 |
| Kernighan 位元計數 | O(設為 1 的位元數) | 在 Python 3.10+ 中,int.bit_count() 更快。 |
| 用遮罩列舉所有子集 | O(n × 2ⁿ) | 共 2ⁿ 個遮罩,每個遮罩要做 n 次位元測試。 |
位元遮罩對每個可能的元素只用一個位元,所以 64 個旗標可以放進單一個 64 位元整數,而不是 64 個獨立的布林變數或串列元素。
常見錯誤
- 負數:Python 整數的行為就像有無限多個符號位元,所以
~5是-6,而bin(-5)是-0b101,不是二補數字串。若要看 32 位元的位元樣式,請先遮罩該值:-5 & 0xFFFFFFFF。 - Python 中負數的右移是算術右移:
-8 >> 1是-4,而-1 >> 10仍是-1。 - 固定位元寬度語言中的溢位:在 Java 或 C 中,32 位元 int 的
1 << 31會變成負數或未定義行為;請使用 64 位元型別,並寫成1L << 40或1ULL << 40。 - 混淆邏輯運算子與位元運算子:
and和or(C 系語言中的&&、||)回傳真假值,而&和|是逐位元運算。 - 可讀性:把
x * 2換成x << 1,在現代編譯器或 Python 中並不會更快,反而讓程式碼更難閱讀。
實際應用
- Unix 檔案權限:
chmod 754設定三組 3 位元的權限(rwx = 4 + 2 + 1)。 - 網路:IPv4 子網路遮罩,其中
ip & mask可得出 CIDR 區塊的網路位址。 - 旗標與選項:Python 的
re.IGNORECASE | re.MULTILINE與enum.Flag以 OR 組合選項。 - 圖形與顏色:24 位元 RGB 值以
(r << 16) | (g << 8) | b打包紅、綠、藍。 - 資料結構與演算法:布隆過濾器、位元集、Fenwick 樹(使用
i & -i)、雜湊與檢查碼。 - 面試題目:只出現一次的數字、計算位元數、2 的冪、反轉位元、缺失的數字、用位元遮罩產生子集、不用
+計算兩整數之和。
常見問題
x & (x - 1) 有什麼作用?
它會清除 x 最低位的 1。可用來計算 1 的位元數,也可用來判斷正數 x 是否為 2 的冪,結果為 0 時即為真。
為什麼 Python 中 ~5 等於 -6?
整數採用二補數,其中 ~x 等於 -x - 1。Python 把這個規則套用在大小無上限的整數上,所以翻轉 5 的每一個位元會得到 -6。
如何在 Python 中計算數字有幾個 1 位元?
在 Python 3.10 以上使用 x.bit_count(),舊版本則用 bin(x).count("1")。對負數而言,bit_count() 計算的是絕對值的位元數。
位元運算比算術運算更快嗎?
對固定大小的整數而言,位元運算只需一條指令,但編譯器最佳化後,加法和乘以常數同樣也是。真正的好處是節省儲存空間與常數空間的演算法,而不是取代一般的算術運算。