位操作基礎#
基本運算子#
Kotlin 的位運算不是符號運算子,而是
Int/Long上的中綴函式:and、or、xor、inv()、shl、shr、ushr。下表範例用0b前綴表示二進位字面值。
| 運算子 | 名稱 | 規則 | 範例 |
|---|---|---|---|
and | AND | 兩位都為 1 才為 1 | 0b1010 and 0b1100 = 0b1000 |
or | OR | 有一位為 1 就為 1 | 0b1010 or 0b1100 = 0b1110 |
xor | XOR | 相異為 1,相同為 0 | 0b1010 xor 0b1100 = 0b0110 |
inv() | NOT | 0 變 1,1 變 0 | 0b1010.inv()(逐位取反) |
shl | 左移 | 左移 n 位 | 0b0011 shl 2 = 0b1100 |
shr | 算術右移 | 右移 n 位,補符號位 | 0b1100 shr 2 = 0b0011 |
ushr | 邏輯右移 | 右移 n 位,高位補 0 | (-1) ushr 28 = 0b1111 |
XOR 的特殊性質#
XOR(異或)在演算法中極為常用:
| 性質 | 公式 | 說明 |
|---|---|---|
| 與 0 異或 | x xor 0 = x | 保持原值 |
| 與全 1 異或 | x xor 1s = x.inv() | 相當於取反 |
| 自我異或 | x xor x = 0 | 結果為 0 |
| 交換律 | a xor b = b xor a | |
| 結合律 | (a xor b) xor c = a xor (b xor c) | |
| 可逆性 | A xor B = C ➡️ C xor B = A | 經典:不用臨時變數交換兩數 |
XOR 交換兩數
// 不使用臨時變數交換 a 和 b
a = a xor b // a = a^b
b = a xor b // b = a^b^b = a
a = a xor b // a = a^b^a = b三大核心技巧#
判斷奇偶性:x and 1#
最後一位為 1 是奇數,為 0 是偶數。
if ((x and 1) == 1) {
// 奇數
} else {
// 偶數
}
x and 1比x % 2更高效(直接位操作 vs 除法運算)。
清除最低位的 1:x and (x - 1)#
將 x 二進位中最右邊的 1 變為 0。
x = 1010...1000
x-1 = 1010...0111
x and x-1 = 1010...0000應用:計算二進位中 1 的個數(Hamming Weight)
var count = 0
while (x != 0) {
x = x and (x - 1)
count++
}取得最低位的 1:x and -x#
取出 x 二進位中最右邊的 1(Lowbit)。
x = ...10100
-x = ...01100 (補碼 = 取反 + 1)
x and -x = ...00100應用:樹狀陣列(Binary Indexed Tree)
進階操作#
更多位操作技巧
| 操作 | 公式 | 說明 |
|---|---|---|
| 取得第 n 位 | (x shr n) and 1 | 0 或 1 |
| 設置第 n 位為 1 | x or (1 shl n) | |
| 清除第 n 位 | x and (1 shl n).inv() | |
| 切換第 n 位 | x xor (1 shl n) | 0➡️1, 1➡️0 |
| 保留最低 n 位 | x and ((1 shl n) - 1) | |
| 判斷是否為 2 的冪 | x > 0 && (x and (x - 1)) == 0 |
進行移位操作時,注意整數型態的位數限制和符號位處理,避免溢位或非預期結果。
位操作速記表#
十進位 二進位
6 = 110
11 = 1011
-1 = 1111...1111 (全1,補碼表示)在二補數表示法中,
-1的二進位表示是全 1(111...111),這是常見的面試考點。
單一位元的四件套#
對「第 i 位」的所有操作,本質上都圍繞同一個工具:把 1 左移到第 i 位做出一個 mask(1 shl i),再用適當的運算子把它套上去。把這四個操作放在一起看,規律就很清楚:
| 操作 | 公式 | mask 與運算子的搭配 |
|---|---|---|
| 取得 (get) | (num shr i) and 1 | 先把第 i 位移到最低位,再用 and 1 篩出來 |
| 設定 (set) | num or (1 shl i) | mask 在第 i 位是 1,用 or 把該位拉成 1 |
| 清除 (clear) | num and (1 shl i).inv() | (1 shl i).inv() 只有第 i 位是 0,用 and 把它壓成 0 |
| 翻轉 (toggle) | num xor (1 shl i) | mask 在第 i 位是 1,用 xor 讓該位 0↔️1 |
fun get(num: Int, i: Int): Int = (num shr i) and 1
fun setBit(num: Int, i: Int): Int = num or (1 shl i)
fun clear(num: Int, i: Int): Int = num and (1 shl i).inv()
fun toggle(num: Int, i: Int): Int = num xor (1 shl i)重點不是死背這四行,而是理解「mask 怎麼建」。記住三件事就能自己推:
1 shl i把目標位點亮;and mask用來「篩出/保留」、or mask用來「設為 1」、xor mask用來「翻轉」;要把某位清成 0,就把 mask 取反(.inv())再and。
常用慣用法#
這些慣用法在面試與競賽裡反覆出現,值得形成肌肉記憶——但更重要的是看懂它們為什麼成立。
判斷是否為 2 的冪#
fun isPowerOfTwo(n: Int): Boolean {
return n > 0 && (n and (n - 1)) == 0
}2 的冪在二進位中只有一個 1(如 1000)。而 n and (n - 1) 會清掉最低位的那個 1(減一會把最低位的 1 借成 0、其右側全變 1,再 and 就把這個 1 連同右側一起歸零)。所以若 n 是 2 的冪,清掉唯一的 1 後就變成 0。前面的 n > 0 用來排除 0 與負數。
不用暫存變數交換兩數#
a = a xor b // a = a^b
b = b xor a // b = (a^b)^b = a
a = a xor b // a = (a^b)^a = b靠的是 XOR 的可逆性(x xor x = 0、x xor 0 = x):第二步把原本的 a 還原到 b、第三步把原本的 b 還原到 a。這是「同一份資料用可逆運算來回變換、不需額外狀態」的典型案例,與狀態抽象與狀態圖裡用等價變換在狀態間移動的想法一脈相承。
XOR 交換在
a與b是**同一個變數(或別名)**時會把值清成 0,因此實務上仍以暫存變數交換為主,這個技巧的價值在於展示 XOR 的可逆性。
取最低位的 1#
val lowbit = n and -n-n 是 n 的二補數(取反加一),這使得 n 與 -n 只在「最低位的 1」這一位上同時為 1,and 之後就只留下那一位。例如 n = ...10100,-n = ...01100,n and -n = ...00100。這是樹狀陣列(Binary Indexed Tree)走訪節點的核心操作。
算術右移 vs 邏輯右移#
Kotlin 和 Java、C 一樣是定寬整數語言:Int 固定 32 位、Long 固定 64 位。針對右移,Kotlin 貼心地提供兩個不同的中綴運算子——shr(算術右移) 對負數在高位「補符號位(補 1)」,等同除以 2 並向下取整;ushr(邏輯右移,unsigned shift right) 一律在高位「補 0」,把符號位當成一般的位元。
| 語言 | 算術右移 | 邏輯右移 | 對負數的行為 |
|---|---|---|---|
| Kotlin | shr | ushr | shr 補符號位仍為負;ushr 高位補 0 變成大正數 |
| Java / C | >> | >>>(Java) | 同 Kotlin,shr/ushr 分別對應 >>/>>> |
| Python | >> | 無,需用 & 0xFFFF… 遮罩模擬 | 只有 >>,一律算術右移,補符號位 |
val x = -8 // 32-bit 補碼:11111111_11111111_11111111_11111000
println(x shr 1) // -4,算術右移補符號位,等同 -8 / 2 向下取整
println(x ushr 1) // 2147483644,邏輯右移高位補 0,把符號位當成一般的 1Kotlin 的
shr是算術右移:對負數會補符號位,等同除以 2 並向下取整(注意是向下取整,不是向零取整:-7 shr 1 == -4而非-3)。若想要定寬語言的邏輯右移(高位補 0),直接用ushr即可——不必像 Python 那樣手動and 0xFFFFFFFF遮罩再右移。從 Java/C 移植依賴>>>的位運算程式時,把>>>直接對應成 Kotlin 的ushr;別誤用shr,否則負數的符號位會一路延伸,得到非預期結果——這是跨語言移植位操作最常見的陷阱之一。