位運算是咱們在編程中常會遇到的操做,但仍然有不少開發者並不瞭解位運算,這就致使在遇到位運算時會「打退堂鼓」。實際上,位運算並無那麼複雜,只要咱們瞭解其運算基礎和運算符的運算規則,就可以掌握位運算的知識。接下來,咱們一塊兒學習位運算的相關知識。python
程序中的數在計算機內存中都是以二進制的形式存在的,位運算就是直接對整數在內存中對應的二進制位進行操做。算法
注意:本文只討論整數運算,小數運算不在本文研究之列編程
咱們經常使用的 3
, 5
等數字是十進制表示,而位運算的基礎是二進制。即人類採用十進制,機器採用的是二進制,要深刻了解位運算,就須要瞭解十進制和二進制的轉換方法和對應關係。bash
十進制轉二進制時,採用「除 2 取餘,逆序排列」法:微信
排序結果就是該十進制數的二進制表示。例如十進制數 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
。學習
其完整過程以下圖所示:ui
有網友整理了常見的進制與 ASCII 碼對照表,表內容以下:spa
ASCII 控制字符code
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
語言中的地板除。
超酷人生 我用 Rust
韋世東|奎因 邀請你關注微信公衆號:Rust之禪
在掌握了位運算的知識後,咱們能夠在開發中嘗試使用它。坊間一直流傳着位運算的效率高,速度快,但從未見過文獻證實,因此本文不討論效率和速度的問題。若是正在閱讀文章的你有相關文獻,請留言告知,謝謝。
判斷數字奇偶
一般,咱們會經過取餘來判斷數字是奇數仍是偶數。例如判斷 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
。這樣的案例還有不少,此處再也不贅述。
至此,咱們已經對位運算有了必定的瞭解,但願你在工做中使用位運算。更多 Saoperation
和知識請掃描下方二維碼。