位運算是我們在編程中常會遇到的操作,但仍然有很多開發者並不了解位運算,這就導致在遇到位運算時會“打退堂鼓”。實際上,位運算並沒有那么復雜,只要我們了解其運算基礎和運算符的運算規則,就能夠掌握位運算的知識。接下來,我們一起學習位運算的相關知識。
程序中的數在計算機內存中都是以二進制的形式存在的,位運算就是直接對整數在內存中對應的二進制位進行操作。
注意:本文只討論整數運算,小數運算不在本文研究之列
位運算的基礎
我們常用的 3, 5 等數字是十進制表示,而位運算的基礎是二進制。即人類采用十進制,機器采用的是二進制,要深入了解位運算,就需要了解十進制和二進制的轉換方法和對應關系。
二進制
十進制轉二進制時,采用“除 2 取余,逆序排列”法:
- 用 2 整除十進制數,得到商和余數;
- 再用 2 整除商,得到新的商和余數;
- 重復第 1 和第 2 步,直到商為 0;
- 將先得到的余數作為二進制數的高位,后得到的余數作為二進制數的低位,依次排序;
排序結果就是該十進制數的二進制表示。例如十進制數 101 轉換為二進制數的計算過程如下:
101 % 2 = 50 余 1
50 % 2 = 25 余 0
25 % 2 = 12 余 1
12 % 2 = 6 余 0
6 % 2 = 3 余 0
3 % 2 = 1 余 1
1 % 2 = 0 余 1
逆序排列即二進制中的從高位到低位排序,得到 7 位二進制數為 1100101,如果要轉換為 8 位二進制數,就需要在最高位補 0。即十進制數的 8 位二進制數為 01100101。
其完整過程如下圖所示:

有網友整理了常見的進制與 ASCII 碼對照表,表內容如下:
ASCII 控制字符

ASCII 可顯示字符

補碼
現在,我們已經了解到二進制與十進制的換算方法,並擁有了進制對照表。但在開始學習位運算符之前,我們還需要了解補碼的知識。
數值有正負之分,那么僅有 0 和 1 的二進制如何表示正負呢?
人們設定,二進制中最高位為 0 代表正,為 1 則代表負。例如 0000 1100 對應的十進制為 12,而 1000 1100 對應的十進制為 -12。這種表示被稱作原碼。但新的問題出現了,原本二進制的最高位始終為 0,為了表示正負又多出了 1,在執行運算時就會出錯。舉個例子,1 + (-2) 的二進制運算如下:
0000 0001 + 1000 0010
= 1000 0011
= -3
這顯然是有問題的,問題就處在這個代表正負的最高位。接着,人們又弄出了反碼(二進制各位置的 0 與 1 互換,例如 0000 1100 的反碼為 1111 0011)。此時,運算就會變成這樣:
0000 0001 + 1111 1101
= 1111 1110
# 在轉換成十進制前,需要再次反碼
= 1000 0001
= -1
這次好像正確了。但它仍然有例外,我們來看一下 1 + (-1):
0000 0001 + 1111 + 1110
= 1111 1111
= 1000 0000
= -0
零是沒有正負之分的,為了解決這個問題,就搞出了補碼的概念。補碼是為了讓負數變成能夠加的正數,所以 負數的補碼= 負數的絕對值取反 + 1,例如 -1 的補碼為:
-1 的絕對值 1
= 0000 0001 # 1 的二進制原碼
= 1111 1110 # 原碼取反
= 1111 1111 # +1 后得到補碼
-1 補碼推導的完整過程如下圖所示:

反過來,由補碼推導原碼的過程為 原碼 = 補碼 - 1,再求反。要注意的是,反碼過程中,最高位的值不變,這樣才能夠保證結果的正負不會出錯。例如 1 + (-6) 和 1 + (-9) 的運算過程如下:
# 1 的補碼 + -6 的補碼
0000 0001 + 1111 1010
= 1111 1011 # 補碼運算結果
= 1111 1010 # 對補碼減 1,得到反碼
= 1000 0101 # 反碼取反,得到原碼
= -5 # 對應的十進制
# 1 的補碼 + -9 的補碼
0000 0001 + 1111 0111
= 1111 1000 # 補碼運算結果
= 1111 0111 # 對補碼減 1,得到反碼
= 1000 1000 # 反碼取反,得到原碼
= -8 # 對應的十進制
要注意的是,正數的補碼與原碼相同,不需要額外運算。也可以說,補碼的出現就是為了解決負數運算時的符號問題。
人生苦短 我用 Python。
崔慶才|靜覓 邀請你關注微信公眾號:進擊的Coder
運算符介紹
位運算分為 6 種,它們是:
| 名稱 | 符號 |
|---|---|
| 按位與 | & |
| 按位或 | | |
| 按位異或 | ^ |
| 按位取反 | ~ |
| 左移運算 | << |
| 右移運算 | >> |
按位與
按位與運算將參與運算的兩數對應的二進制位相與,當對應的二進制位均為 1 時,結果位為 1,否則結果位為 0。按位與運算的運算符為 &,參與運算的數以補碼方式出現。舉個例子,將數字 5 和數字 8 進行按位與運算,其實是將數字 5 對應的二進制 0000 0101 和數字 8 對應的二進制 0000 1000 進行按位與運算,即:
0000 0101
&
0000 1000
根據按位與的規則,將各個位置的數進行比對。運算過程如下:
0000 0101
&
0000 1000
---- ----
0000 0000
由於它們對應位置中沒有“均為 1 ”的情況,所以得到的結果是 0000 0000。數字 5 和 8 按位與運算的完整過程如下圖:

將結果換算成十進制,得到 0,即 5&8 = 0。
按位或
按位或運算將參與運算的兩數對應的二進制位相或,只要對應的二進制位中有 1,結果位為 1,否則結果位為 0。按位或運算的運算符為 |,參與運算的數以補碼方式出現。舉個例子,將數字 3 和數字 7 進行按位或運算,其實是將數字 3 對應的二進制 0000 0011和數字 7 對應的二進制 0000 0111 進行按位或運算,即:
0000 0011
|
0000 0111
根據按位或的規則,將各個位置的數進行比對。運算過程如下:
0000 0011
|
0000 0111
---- ----
0000 0111
最終得到的結果為 0000 0111。將結果換算成十進制,得到 7,即 3|7 = 7。
按位異或
按位異或運算將參與運算的兩數對應的二進制位相異或,當對應的二進制位值不同時,結果位為 1,否則結果位為 0。按位異或的運算符為 ^,參與運算的數以補碼方式出現。舉個例子,將數字 12 和數字 7 進行按位異或運算,其實是將數字 12 對應的二進制 0000 1100 和數字 7 對應的二進制 0000 0111 進行按位異或運算,即:
0000 1100
^
0000 0111
根據按位異或的規則,將各個位置的數進行比對。運算過程如下:
0000 1100
^
0000 0111
---- ----
0000 1011
最終得到的結果為 0000 1011。將結果換算成十進制,得到 11,即 12^7 = 11。
按位取反
按位取反運算將二進制數的每一個位上面的 0 換成 1,1 換成 0。按位取反的運算符為 ~,參與運算的數以補碼方式出現。舉個例子,對數字 9 進行按位取反運算,其實是將數字 9 對應的二進制 0000 1001 進行按位取反運算,即:
~0000 1001
= 0000 1001 # 補碼,正數補碼即原碼
= 1111 1010 # 取反
= -10
最終得到的結果為 -10。再來看一個例子,-20 按位取反的過程如下:
~0001 0100
= 1110 1100 # 補碼
= 0001 0011 # 取反
= 19
最終得到的結果為 19。我們從示例中找到了規律,按位取反的結果用數學公式表示:![]()


我們可以將其套用在 9 和 -20 上:
~9 = -(9 + 1) = -10
~(-20) = -((-20) + 1) = 19
這個規律也可以作用於數字 0 上,即 ~0 = -(0 + 1) = -1。
左移運算
左移運算將數對應的二進位全部向左移動若干位,高位丟棄,低位補 0。左移運算的運算符為 <<。舉個例子,將數字 5 左移 4 位,其實是將數字 5 對應的二進制 0000 0101 中的二進位向左移動 4 位,即:
5 << 4
= 0000 0101 << 4
= 0101 0000 # 高位丟棄,低位補 0
= 80
數字 5 左移 4 位的完整運算過程如下圖:

最終結果為 80。這等效於:
![]()
也就是說,左移運算的規律為:
![]()
右移運算
右移運算將數對應的二進位全部向右移動若干位。對於左邊的空位,如果是正數則補 0,負數可能補 0 或 1 (Turbo C 和很多編譯器選擇補 1)。右移運算的運算符為 >>。舉個例子,將數字 80 右移 4 位,其實是將數字 80 對應的二進制 0101 0000 中的二進位向右移動 4 位,即:
80 >> 4
= 0101 0000 >> 4
= 0000 0101 # 正數補0,負數補1
= 5
最終結果為 5。這等效於:

也就是說,右移運算的規律為:

要注意的是,不能整除時,取整數。這中除法取整的規則類似於 PYTHON 語言中的地板除。
位運算的應用
在掌握了位運算的知識后,我們可以在開發中嘗試使用它。坊間一直流傳着位運算的效率高,速度快,但從未見過文獻證明,所以本文不討論效率和速度的問題。如果正在閱讀文章的你有相關文獻,請留言告知,謝謝。
判斷數字奇偶
通常,我們會通過取余來判斷數字是奇數還是偶數。例如判斷 101 的奇偶用的方法是:
# python
if 101 % 2:
print('偶數')
else:
print('奇數')
我們也可以通過位運算中的按位與來實現奇偶判斷,例如:
# python
if 101 & 1:
print('奇數')
else:
print('偶數')
這是因為奇數的二進制最低位始終為 1,而偶數的二進制最低為始終為 0。所以,無論任何奇數與 1 即 0000 0001 相與得到的都是 1,任何偶數與其相與得到的都是 0。
變量交換
在 C 語言中,兩個變量的交換必須通過第三個變量來實現。偽代碼如下:
# 偽代碼
a = 3, b = 5
c = a
a = b
b = a
--------
a = 5, b = 3
在 PYTHON 語言中並沒有這么麻煩,可以直接交換。對應的 PYTHON 代碼如下:
# python
a, b = 3, 5
a, b = b, a
print(a, b)
代碼運行結果為 5 3。但大部分編程語言都不支持 PYTHON 這種寫法,在這種情況下我們可以通過位運算中的按位異或來實現變量的交換。對應的偽代碼如下:
# 偽代碼
a = 3, b = 5
a = a ^ b
b = a ^ b
a = a ^ b
最后,a = 5, b = 3。我們可以用 C 語言和 PYTHON 語言進行驗證,對應的 PYTHON 代碼如下:
# python
a, b = 3, 5
a = a ^ b
b = a ^ b
a = a ^ b
print(a, b)
代碼運行結果為 5 3,說明變量交換成功。對應的 C 代碼如下:
#include<stdio.h>
void main()
{
int a = 3, b = 5;
printf("交換前:a=%d , b=%d\n",a,b);
a = a^b;
b = a^b;
a = a^b;
printf("交換后:a=%d , b=%d\n",a, b);
}
代碼運行結果如下:
交換前:a=3 , b=5
交換后:a=5 , b=3
這說明變量交換成功。
求 x 與 2 的 n 次方乘積
設一個數為 x,求 x 與 2 的 n 次方乘積。這用數學來計算都是非常簡單的:
![]()
在位運算中,要實現這個需求只需要用到左移運算,即 x << n。
取 x 的第 k 位
即取數字 x 對應的二進制的第 k 位上的二進制值。假設數字為 5,其對應的二進制為 0000 0101,取第 k 位二進制值的位運算為 x >> k & 1。我們可以用 PYTHON 代碼進行驗證:
# python
x = 5 # 0000 0101
for i in range(8):
print(x >> i & 1)
代碼運行結果如下:
1
0
1
0
0
0
0
0
這說明位運算的算法是正確的,可以滿足我們的需求。
判斷賦值
if a == x:
x = b
else:
x = a
等效於 x = a ^ b ^ x。我們可以通過 PYTHON 代碼來驗證:
# python
a, b, x = 6, 9, 6
if a == x:
x = b
else:
x = a
print(a, b, x)
代碼運行結果為 699,與之等效的代碼如下:
# python
a, b, x = 6, 9, 6
x = a ^ b ^ x
print(a, b, x)
這樣就省去了 if else 的判斷語句。
代替地板除
二分查找是最常用的算法之一,但它有一定的前提條件:二分查找的目標必須采用順序存儲結構,且元素有序排列。例如 PYTHON 中的有序列表。二分查找的最優復雜度為 O(1),最差時間復雜度為 O(log n)。舉個例子,假設我們需要從列表 [1, 3, 5, 6, 7, 8, 12, 22, 23, 43, 65, 76, 90, 543] 中找到指定元素的下標,對應的 PYTHON 代碼如下:
# python
def search(lis: list, x: int) -> int:
"""非遞歸二分查找
返回指定元素在列表中的索引
-1 代表不存在"""
mix_index = 0
max_index = len(lis) - 1
while mix_index <= max_index:
midpoint = (mix_index + max_index) // 2
if lis[midpoint] < x:
mix_index = mix_index + 1
elif lis[midpoint] > x:
max_index = max_index - 1
else:
return midpoint
return -1
lists = [1, 3, 5, 6, 7, 8, 12, 22, 23, 43, 65, 76, 90, 543]
res = search(lists, 76)
print(res)
在取列表中間值時使用的語句是 midpoint = (mix_index + max_index) // 2,即地板除,我們可以將其替換為 midpoint = (mix_index + max_index) >> 1 最終得到的結果是相同的。這是因為左移 1位 等效於乘以 2,而右移 1 位等效於除以 2。這樣的案例還有很多,此處不再贅述。
至此,我們已經對位運算有了一定的了解,希望你在工作中使用位運算。
作者:華為雲雲享專家 韋世東
