一篇讀懂Python中的位運算


什么是位運算?

簡單來說,位運算是把數字轉換為機器語言,也就是二進制來進行計算的一種運算形式。
在古老的微處理上,位運算比加減運算略快,要比乘除運算快的多。雖然現在隨着技術的迭代,新的架構在推陳出新,位運算與加減法相差無幾,但是仍然快於乘除運算。為什么這么說呢?因為位運算符直接處理每一個比特位(bit),這么底層的運算,當然快了!但是缺點也很明顯,理解起來稍顯復雜,不夠直觀。這在許多的場合都不使用它們, 因為它會增加代碼難度和排錯!不利於粘貼和復制(純屬扯淡!)。
首先,我們要明白一點,位運算符之對整數起作用,如果一個操作數(如浮點數)不是整數,那它首先會自動轉換為整數后再執行。另外Python語言參考中也強烈表示只能是整數!


我想,你可能要試試看:

>>> c = 1.2
>>> d = 3.5
>>> c | d
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'float' and 'float'

要想玩明白位運算,我們要回顧一些基礎知識。

用到的基礎知識

問題來了:計算機內部是如何用二進制表示整數的

讓我們思考一個問題,計算機內部是如何用二進制表示這些整數的?計算機內用定點數表示的。那問題又來了,什么是定點數?
在計算機內,定點數有3種表示法:原碼、反碼和補碼。
問題一個接着一個,原碼、反碼、補碼又是什么鬼東西?媽媽呀,還讓我回去搬磚吧!

原碼

原碼(true form)是一種計算機中對數字的二進制定點表示方法。原碼表示法在數值前面增加了一位符號位(即最高位為符號位):正數該位為0,負數該位為1(0有兩種表示:+0和-0),其余位表示數值的大小。
口訣在此:一個正數,轉換為二進制位就是這個正數的原碼。負數的絕對值轉換成二進制位然后在高位補1就是這個負數的原碼。
比如說int類型的3的原碼是11B(B表示二進制位,這里你可以多了解一些進制之間的轉換),在32為機器上占4個字節,所以,高位補0就是:

00000000 00000000 00000000 00000011	# 一個字節8個bit位

那么int類型的-3的絕對值的二進制位就是11B展開后最高位補1就是:

10000000 00000000 00000000 00000011

但是呢,原碼也有缺點,原碼中的0分為+0-0。不僅如此,在進行不同符號的加法運算或者同符號的減法運算時,不能直接判斷出結果的正負,我們必須要將兩個值的絕對值進行比較。然后再進行加減操作。最后符號由絕對值大的決定,於是乎,下面有請反碼登場。

反碼

反碼是數值存儲的一種,多應用於系統環境設置,如linux平台的目錄和文件的默認權限的設置umask,就是使用反碼原理。
口訣不能忘:正數的反碼就是原碼,負數的反碼等於原碼除符號位以外所有位取反。
比如還是剛才的那個int類型的3的反碼是:

00000000 00000000 00000000 00000011	 # 正數的反碼就是原碼,這么寫沒毛病

int類型的-3的反碼是,讓我們默念公式:負數的反碼等於原碼除符號位以外所有位取反!

10000000 00000000 00000000 00000011	 # -3的原碼
11111111 11111111 11111111 11111100	 # 最高位為符號位,不變,其余取反

這樣,反碼解決了加減法運算問題(不理解就自己再查吧),我們就該着手處理+0-0的問題了,有請補碼上台領獎!

補碼

在計算機系統中,數值一律用補碼來表示和存儲。原因在於,使用補碼,可以將符號位和數值域統一處理;同時,加法和減法也可以統一處理。此外,補碼與原碼相互轉換,其運算過程是相同的,不需要額外的硬件電路支持。
記住口訣:正數的補碼與原碼相同,負數的補碼為其原碼除符號位外所有位取反(這就是反碼了),然后最低位加1。
還是那個int類型的3的補碼是:

00000000 00000000 00000000 00000011	 # 正數的補碼與原碼一致

那么int類型的-3的補碼就是,讓我們手掐口訣:

10000000 00000000 00000000 00000011  # -3的原碼
11111111 11111111 11111111 11111100	 # 負數的補碼為其原碼除符號位外所有位取反
11111111 11111111 11111111 11111101  # 然后最低位加1,完美!

原、反、補碼小結

原、反、補碼小結:

  • 正數的反碼和補碼都與原碼相同
  • 負數的反碼為該數的原碼除符號位外所有位取反
  • 負數的補碼為該數的原碼除符號位外所有位取反,然后最低位加1

優缺點:

  • 原碼最好理解,但是存在加減法運算不方便的問題,還有倆零蛋搗亂
  • 反碼稍微難點,但僅解決了加減法的問題。倆零繼續搗亂
  • 補碼理解相對困難,但解決了上面的倆問題

單、雙、三目運算

根據操作數的個數,運算符可以分為單目、雙目、三目運算符,也稱為一元、二元、三元運算符等。若完成一個操作需要兩個操作數,則稱該運算符為雙目運算符;若完成一個操作需要一個操作數,則稱該運算符為單目運算符。

Python中的按位運算

我們在之前的原、反、補碼中了解了基本的數值存儲。那么這里就開始具體的學習Python中按位是怎么運算的,首先來看規則。Python中的按位運算規則如下表所示:

運算符 描述
& 按位運算符,參與運算的兩個值,如果相應位都為1,則該位的結果為1,否則為0
^ 按位異或運算符,當兩個對應的二進位相異時,結果為1
~ 按位取反運算符,對數據的每個二進制位取反,即把1變為0,把0變為1
| 按位或運算,只要對應兩個二進制位有一個為1時,結果就為1
<< 左移動運算符:運算數的各二進位全部左移若干位,由 << 右邊的數字指定了移動的位數,高位丟棄,低位補0
>> 右移動運算符:把>>左邊的運算數的各二進位全部右移若干位,>> 右邊的數字指定了移動的位數

在Python的按位運算符中,只有反轉~運算符是單目運算,其余都是雙目運算。

按位與 &

按位與的規則是:參與運算的兩個值,如果相應位都為1,則該位的結果為1,否則為0,也就是說:

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 1 = 0
  • 0 & 0 = 0

我們首先來看一個示例:

>>> 3 & 5
1

分析,我們首先來看它們各自的補碼,我們接下來的演示只用一個字節8位表示就行,32位太長了(搞起來難受):

0000 0011  # 3的補碼
0000 0101  # 5的補碼
0000 0001  # 根據按位與的規則,得出補碼結果

得出的結果是補碼類型的, 我們要先把補碼轉換為原碼,再將二進制轉換為十進制的結果。正數的補碼等於原碼,所以結果就是1
再來個示例:

>>> -2 & -3
-4

老套路,先找各自的補碼,再求結果:

1111 1110	# -2的補碼
1111 1101	# -3的補碼
1111 1100	# 結果

我們將補碼轉換為原碼,默念口訣:補碼轉原碼,符號位不變,數值為按位取反,末位加1:

1111 1100	# 補碼
1000 0011	# 符號位不變,數值位按位取反
1000 0100	# 末位加1

想着最高位的符號位為負,二進制100對應的十進制是4,最終結果就是-4
再來個例子:

>>> -2 & 3
2

老套路,拿到它們各自的補碼,再求結果:

1111 1110	 # -2的補碼
0000 0011	 # 3的補碼
0000 0010	 # 結果

找到對應的十進制是2
小結:在按位與的結果中,只有是負數的情況下,才需要將補碼轉換為原碼,然后再求對應的十進制數。

按位或 |

先把口訣放這里:按位或運算,只要對應兩個二進制位有一個為1時,結果就為1。也就是說:

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 1 = 1
  • 0 | 0 = 0

再把例子拿過來:

>>> 3 | 5
7

拿到補碼:

0000 0011 	# 3的補碼
0000 0101	# 5的補碼
0000 0111	# 結果

二進制的111轉為十進制是7
再來個例子:

>>> -2 | -3
-1

各自的補碼是:

1111 1110	 # -2的補碼
1111 1101	 # -3的補碼
1111 1111	 # 結果

拿到了結果,我們還需要將補碼轉換為原碼再轉10進制:

1111 1111	# 結果
1000 0000	# 高位不變,其余取反
1000 0001	# 末位加1

最高位的是負號,最終的結果是-1
再來個例子:

>>> -2 | 3
-1

老套路,拿到它們各自的補碼,再求結果:

1111 1110	# -2的補碼
0000 0011	# 3的補碼
1111 1111	# 結果

繼續補碼轉原碼再轉十進制:

1111 1111	# 結果
1000 0000	# 高位不變,其余取反
1000 0001	# 末位加1

最高位為負號,找到對應的十進制是-1

按位異或 ^

先把規則列出來:按位異或運算符,當兩個對應的二進位相異時,結果為1,也就是說:

  • 1 ^ 1 = 0
  • 1 ^ 0 = 1
  • 0 ^ 1 = 1
  • 0 ^ 0 = 0

再把例子拿過來:

>>> 3 ^ 5
6

拿到補碼:

0000 0011 	# 3的補碼
0000 0101	# 5的補碼
0000 0110	# 結果,注意按照規則來

正整數的結果一目了然,二進制的110轉為十進制是6
再來個例子:

>>> -2 ^ -3
3

各自的補碼是:

1111 1110	# -2的補碼
1111 1101	# -3的補碼
0000 0011	# 結果

首先來看011對應的十進制是3,所以最終結果是3
再來個例子:

>>> -2 ^ 3
-3

老套路,拿到它們各自的補碼,再求結果:

1111 1110	# -2的補碼
0000 0011	# 3的補碼
1111 1101	# 結果

結果是負數,只能將補碼轉原碼再轉10進制了:

1111 1101	# 結果
1000 0010	# 高位不變,其余取反
1000 0101	# 末位加1

最高位為負號,二進制的1013, 所以對應的十進制是-3
最后,來總結一下異或特點,0異或任何數得這個數(0異或0得0),一個數與自己異或時結果為0

>>> 0 ^ 0
0
>>> 0 ^ 3
3
>>> 0 ^ -3
-3
>>> 3 ^ 3
0

按位取反 ~

首先來說,按位取反是單目運算。所以,別上來就:

>>> 2 ~ 3
  File "<stdin>", line 1
    2 ~ 3
      ^
SyntaxError: invalid syntax

顯得可low了,一點都不!專!業!!!

書歸正傳,先把規則列出來:按位取反運算符,對數據的每個二進制位取反,即把1變為0,把0變為1
來個例子:

>>> ~ 3
-4

拿到-3的補碼:

0000 0011 	# 3的補碼

按每個二進制位取反:

1111 1100	# 結果是負數,還要轉為原碼
1000 0011	# 高位不變,其余取反
1000 0100	# 末位加一

別忘了最高位的負號,二進制的100轉為十進制是-4
再來個例子:

>>> ~ -2
1

-2的補碼是:

1111 1110	 # -2的補碼

按位取反:

0000 0001

得到的結果一目了然,是1

按位左移 <<

先把規則列出來:左移動運算符,運算數的各二進位全部左移若干位,而 << 右邊的數字指定了移動的位數,高位丟棄,低位補0
來個示例:

>>> 2 << 3
16

先拿到2的補碼:

0000 0010	# 2的補碼

整體(這里也就是1)開始往左移動,移動的位數是3位,所以得的移動結果:

0001 0000

最終的十進制結果是16

按位右移 >>

先把規則列出來:右移動運算符,把>>左邊的運算數的各二進位全部右移若干位,>> 右邊的數字指定了移動的位數
來個示例:

>>> 2 >> 3
0

先拿到2的補碼:

0000 0010	# 2的補碼

從1(從右往左數,第二位)開始往右移動,移動的位數是3位,所以得的移動結果:

0000 0000  

移動到第3位時,把1就移沒了,剩下全是0最終的十進制結果是0

位運算的應用

  • 異或用來做加密混淆,比如JavaScript為了防止源碼被盜,除了美化、壓縮就是可以做混淆。包括C中可以做加密算法。
  • 按位與和按位或可以做矩陣、跑馬燈。IOS中可以用來控制按鈕的操作。
  • 可以制定不同的規則,來通過一串二進制就可以表示不同的狀態信息,比如一串32位的二級制位,就可以有表現32個狀態信息。

隨便舉幾個示例

  • 示例1:來自leetcode中的一個題。題目是給定一個非空的數組,除了一個元素外,其余的元素都會出現兩次,找到這個僅出現一次的元素。注意:你的算法應具有線性運行時的復雜性O(n),如果不能有額外的內存開銷更好。我們的老套路是:
def f1():
    l1 = [1, 1, 2, 2, 3, 4, 4, 5, 5]
    s1 = ''.join([str(i) for i in l1])
    for i in s1:
        if s1.count(i) == 1:
            return i

我們可以用異或的特點(0異或任何數得這個數,一個數與自己異或時結果為0)來幫助我們解決這個問題:

def f2():
    l1 = [1, 2, 1, 2, 3, 4, 3, 4, 5, 5, 6, 7, 6, 8, 8]
    temp = 0
    for i in l1:
        temp ^= i
        print(i, temp)
    return temp
  • 示例2,還是leetcode中的題。給定一個整數,求出該數的二進制數中有多少個1,這里我們可以使用字符串的count來解決該問題:
print('00000000000000000000000000001011'.count('1'))
print(bin(10), bin(10).count('1'))

除此之外,我們也可以使用按位右移和按位與也可以完成該需求,我們只需要將該數不斷地右移,然后和1按位與,知道這個數為0即可:

def f1(n):
    temp = 0
    for i in bin(n):  # 10的二進制是1010
        print(n & 1)
        temp += n & 1
        n = n >> 1
    return temp
print(f1(10))  # 2

see also:[Python語言中的按位運算](https://blog.csdn.net/xtj332/article/details/6639009) | [原碼、反碼、補碼和移碼](https://www.jianshu.com/p/129f9daae472) | [來自JavaScript的二進制位運算符](https://wangdoc.com/javascript/operators/bit.html) | [LeetCode解題中位運算的運用](https://www.cnblogs.com/fengziwei/p/7588271.html) | [leetcode](https://leetcode-cn.com/) | [計算機中原碼,反碼,補碼之間的關系](https://www.cnblogs.com/hanhuo/p/6341111.html) 歡迎斧正,that's all


免責聲明!

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



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