7 分鍾全面了解位運算


位運算是我們在編程中常會遇到的操作,但仍然有很多開發者並不了解位運算,這就導致在遇到位運算時會“打退堂鼓”。實際上,位運算並沒有那么復雜,只要我們了解其運算基礎和運算符的運算規則,就能夠掌握位運算的知識。接下來,我們一起學習位運算的相關知識。

程序中的數在計算機內存中都是以二進制的形式存在的,位運算就是直接對整數在內存中對應的二進制位進行操作。

注意:本文只討論整數運算,小數運算不在本文研究之列

位運算的基礎

我們常用的 35 等數字是十進制表示,而位運算的基礎是二進制。即人類采用十進制,機器采用的是二進制,要深入了解位運算,就需要了解十進制和二進制的轉換方法和對應關系。

二進制

十進制轉二進制時,采用“除 2 取余,逆序排列”法:

  1. 用 2 整除十進制數,得到商和余數;
  2. 再用 2 整除商,得到新的商和余數;
  3. 重復第 1 和第 2 步,直到商為 0;
  4. 將先得到的余數作為二進制數的高位,后得到的余數作為二進制數的低位,依次排序;

排序結果就是該十進制數的二進制表示。例如十進制數 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 可顯示字符

補碼

現在,我們已經了解到二進制與十進制的換算方法,並擁有了進制對照表。但在開始學習位運算符之前,我們還需要了解補碼的知識。

數值有正負之分,那么僅有 01 的二進制如何表示正負呢?

人們設定,二進制中最高位為 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 換成 11 換成 0。按位取反的運算符為 ~,參與運算的數以補碼方式出現。舉個例子,對數字 9 進行按位取反運算,其實是將數字 9 對應的二進制 0000 1001 進行按位取反運算,即:

~0000 1001
= 0000 1001 # 補碼,正數補碼即原碼
= 1111 1010 # 取反
= -10

最終得到的結果為 -10。再來看一個例子,-20 按位取反的過程如下:

~0001 0100
= 1110 1100 # 補碼
= 0001 0011 # 取反
= 19

最終得到的結果為 19。我們從示例中找到了規律,按位取反的結果用數學公式表示:~x = -(x + 1)

~x = -(x + 1)

我們可以將其套用在 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,負數可能補 01 (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,求 x2n 次方乘積。這用數學來計算都是非常簡單的:

在位運算中,要實現這個需求只需要用到左移運算,即 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。這樣的案例還有很多,此處不再贅述。

至此,我們已經對位運算有了一定的了解,希望你在工作中使用位運算。


作者:華為雲雲享專家 韋世東


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM