什么是位運算?
簡單來說,位運算是把數字轉換為機器語言,也就是二進制來進行計算的一種運算形式。
在古老的微處理上,位運算比加減運算略快,要比乘除運算快的多。雖然現在隨着技術的迭代,新的架構在推陳出新,位運算與加減法相差無幾,但是仍然快於乘除運算。為什么這么說呢?因為位運算符直接處理每一個比特位(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
最高位為負號,二進制的101
是3
, 所以對應的十進制是-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