7 分鐘全面瞭解位運算

位運算是咱們在編程中常會遇到的操做,但仍然有不少開發者並不瞭解位運算,這就致使在遇到位運算時會「打退堂鼓」。實際上,位運算並無那麼複雜,只要咱們瞭解其運算基礎和運算符的運算規則,就可以掌握位運算的知識。接下來,咱們一塊兒學習位運算的相關知識。python

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

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

位運算的基礎

咱們經常使用的 35 等數字是十進制表示,而位運算的基礎是二進制。即人類採用十進制,機器採用的是二進制,要深刻了解位運算,就須要瞭解十進制和二進制的轉換方法和對應關係。bash

二進制

十進制轉二進制時,採用「除 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學習

其完整過程以下圖所示:ui

有網友整理了常見的進制與 ASCII 碼對照表,表內容以下:spa

ASCII 控制字符code

ASCII 可顯示字符

補碼

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

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

人們設定,二進制中最高位爲 0 表明正,爲 1 則表明負。例如 0000 1100 對應的十進制爲 12,而 1000 1100 對應的十進制爲 -12。這種表示被稱做原碼。但新的問題出現了,本來二進制的最高位始終爲 0,爲了表示正負又多出了 1,在執行運算時就會出錯。舉個例子,1 + (-2) 的二進制運算以下:

0000 0001 + 1000 0010 
= 1000 0011
= -3 
複製代碼

這顯然是有問題的,問題就處在這個表明正負的最高位。接着,人們又弄出了反碼(二進制各位置的 01 互換,例如 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。數字 58 按位與運算的完整過程以下圖:

將結果換算成十進制,獲得 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)

咱們能夠將其套用在 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。這等效於:

5 * (2) ^4

也就是說,左移運算的規律爲:

x << n = x * (2) ^ n

右移運算

右移運算將數對應的二進位所有向右移動若干位。對於左邊的空位,若是是正數則補 0,負數可能補 01 (Turbo C 和不少編譯器選擇補 1)。右移運算的運算符爲 >>。舉個例子,將數字 80 右移 4 位,實際上是將數字 80 對應的二進制 0101 0000 中的二進位向右移動 4 位,即:

80 >> 4
= 0101 0000 >> 4
= 0000 0101 # 正數補0,負數補1 
= 5
複製代碼

最終結果爲 5。這等效於:

80 \div (2)^4

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

x >> n = x \div (2) ^ n

要注意的是,不能整除時,取整數。這中除法取整的規則相似於 PYTHON 語言中的地板除。

超酷人生 我用 Rust

韋世東|奎因 邀請你關注微信公衆號:Rust之禪

位運算的應用

在掌握了位運算的知識後,咱們能夠在開發中嘗試使用它。坊間一直流傳着位運算的效率高,速度快,但從未見過文獻證實,因此本文不討論效率和速度的問題。若是正在閱讀文章的你有相關文獻,請留言告知,謝謝。

判斷數字奇偶

一般,咱們會經過取餘來判斷數字是奇數仍是偶數。例如判斷 101 的奇偶用的方法是:

# python
if 101 % 2:
	print('偶數')
else:
	print('奇數')
複製代碼

咱們也能夠經過位運算中的按位與來實現奇偶判斷,例如:

# python
if 101 & 1:
	print('奇數')
else:
	print('偶數')
複製代碼

這是由於奇數的二進制最低位始終爲 1,而偶數的二進制最低爲始終爲 0。因此,不管任何奇數與 10000 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 * (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 和知識請掃描下方二維碼。

相關文章
相關標籤/搜索