請查收這份“位運算”的裝Bi指南

運算可謂是與編程息息相關,我們編寫的每一個程序可能都帶有加減乘除,當然這是最基礎的運算了。在大一下的時候學了第一門編程語言C,隨着也學到了取餘(%)和三目運算符(? :),當時就覺得(? :)真的NiuBi,但在編程時卻很少用到,因爲if和else已經刻在我的腦子裏。

不同語言中的運算符也會有一些偏差,像Python中的整除(//)是C中沒有的,C中的三目運算符在Python中也有着不同的表現形式,比如np.where和if、else組合。

下面介紹個人認爲比較高大上的位運算符,說它高大上很大一方面是因爲位運算用來解決某些問題十分便捷,而另一小方面就是因爲它可以ZhuangBi。如果你沒接觸過位運算,即使是很少一部分關於位運算的代碼,你也會一臉懵,而在你瞭解位運算後,你會發現這種方法確實比較巧妙。

我們知道所有數字包括字母、符號等在計算機中都是以二進制形式存儲的,而位運算就是直接對二進制進行操作,常見的位運算包括以下幾種:

  • 按位與:&
  • 按位或:|
  • 按位異或:^
  • 左移:<<
  • 右移:>>
  • 取反:~

這些位運算符號按照優先級順序排序如下:

1 2 3 4 5
~ <<、>> & ^ 按位或

爲了便於理解和觀看,下面舉例中只列舉8位的二進制

按位與(&)

按位與運算法則可以概括成“同真則真,反之則假”,在0和1之間的運算,有以下形式:

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

比如數字5和數字6之間採用按位與運算,將兩個數組8位二進制形式的每一位對應,運用上面的法則就可以得出一個新的數字4,即5 & 6 = 4。
在這裏插入圖片描述

按位或(|)

按位或運算法則可以概括成“同假才假,反之則真”,在0和1之間的運算,有以下形式:

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

同樣還用數字5和數字6舉例,利用上述相同方式在二者之間做按位或運算,可以得到一個新的數字7,即5 | 6 = 7。
在這裏插入圖片描述

按位異或(^)

按位異或運算法則可以概括成“相同則真,不同則假”,在0和1之間的運算,有以下形式:

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

仍然還是數字5與數字6爲例利用上述相同方式在二者之間做按位異域運算,可以得到一個新的數字3,即5 ^ 6 = 3。
在這裏插入圖片描述

左移(<<)

左移操作就是將一個數字的二進制形式整體向左移動,然後右邊的虛位補0,具體移動看下圖:

在這裏插入圖片描述

這個例子是2<<2 = 8的體現,可以看到1原本處於右邊第2位,如果整體向左移動兩位,那麼1就移動到了右邊第4位,空下的兩位用0補上即可。

右移(>>)

右移操作與左移操作方式相同,方向相反,具體移動看下圖:

在這裏插入圖片描述

這個例子是6>>2 = 1的體現,只需要整體向右移動兩位,然後左邊虛位補0,不需要顧慮會不會有1被覆蓋。

這兩個符號比較容易混亂,這裏給一個自己的小Tips:箭頭朝哪向哪移,左移值增大,左移值減小。

取反(~)

取反操作就比較簡單了,只需要記住這樣一個小式子: ~x = -(x+1),比如數字5取反操作就等於-6,即~5 = -(5+1) = -6。

已經介紹完了這六個位運算符是如何對二進制進行操作的,可是簡單的介紹並不能體現出位運算的高大上,下面利用位運算的技巧解決一些問題,這些問題並不是很難,但是我們從中可以認識到位運算的便捷,以及加深對位運算操作的印象。

交換兩個數字

對於這個問題可能我們想到的第一個方法就是利用第三個變量來幫助交換,比如若要交換a、b兩個變量的值,就需要定義一個臨時變量temp輔助。

a = 1;b = 2
temp = 0
temp = a; a = b; b = temp

這樣做是需要額外存儲空間的,那有沒有一種方法僅在原地就可以交換a、b兩個變量的值呢?位運算就可以,而且十分簡單。

a = a^b #(1)
b = a^b #(2)
a = a^b #(3)

如果你沒接觸過位運算,對於這三行代碼肯定很懵,讀懂這三行代碼你還需要知道按位異或的幾個性質:

  • 1.按位異或滿足交換律,即a ^ b = b ^ a
  • 2.按位異或滿足結合律,即(a ^ b) ^ c=a ^ (b ^ c)
  • 3.任何數與0異或都等於它自己,比如a ^ 0 = a。

若將式子(1)代入式子(2),利用上述性質並結合按位異或“相同則假”的法則,可以將式子(2)變成以下形式:

b = a^b^b = a^0 = a

這樣a的值就賦給b了,如果再將求出的式子(2)代入式子(3),式子(3)則變成:

a = a^a^b = 0^b = b

這樣就完成了變量a、b之間的調換,是不是已經有NiuBi的感覺了,繼續看下一道。

2的冪

假設給定一個數字,你需要判斷它是否爲2的冪,如果是,需要返回true,反之就返回false。

這個問題利用循環方法也比較不錯,設定一個循環條件,然後讓輸入整數n一直除以2,最後只需要判斷n是否等於1即可,因爲n==1只有當n爲2的冪時才滿足。

while n>1:
    n/=2
return n==1

這種方法應該算是一種很普通的方法了,體現不出自己的逼格怎麼辦?看看利用位運算是怎麼解決的吧。

return n > 0 and n & (n - 1) == 0

如果n是2的冪的話,則其二進制最高位上應該爲1,其餘位置都應該爲0,比如8的二進制爲1000;對於n-1的話,則應該是除了最高位爲0,其餘都爲1,比如7的二進制爲0111,所以此時n&(n-1)=0。

找不同

假如給定一個只包含小寫字母的字符串s1,然後字符串s2是將s1打亂並且向其中插入了一個新的小寫字母,想讓你挑出這個新插入的字母。比如s1 = ‘abc’,s2 = ‘badc’,輸出d。

第一種想法就是利用哈希表來統計並存儲每個元素出現的次數,出現一次的就是新加入的元素,而利用哈希表就意味着需要空間新建字典。而利用按位異或運算只需引入一個第三變量就可以解決這個問題,利用上文提及的異或性質1和3、以及“相同則假”的法則即可。

m = s1+s2
first = 0
for i in range(0,len(m)):
    first ^= ord(m[i])
return chr(first)

這裏ord是將字母轉爲對應的Ascii碼,chr則是將Ascii碼數字轉回爲對應的字母形式。

翻轉二進制

先以8位的二進制舉例,比如輸入的二進制爲000100111,要求輸出爲11001000。

整數翻轉利用字符串就可以很好的解決,但是二進制不同於整數,因爲二進制中有很多0,可能在翻轉並轉化之後有些0會被抹去。這裏就可以利用位運算實現二進制的翻轉,具體代碼如下:

res = 0
for i in range(8):
    res <<= 1
    res += n & 1
    n >>= 1
return bin(res)

這裏利用按位與“同真則真,反之則假”的法則,每次將輸入的二進制最後一位與1比較,得出的結果加至res上,然後將n右移一位,因爲此時最後一位已經比較過了。

在每次遍歷開始時要注意將res左移一位,因爲不論n&1得到的結果是0還是1,它在二進制中都是有意義的,需要爲這個即將得出的數提供一個空地,不可以省略。這裏推薦自己手推一下,很容易就理解了。

1個數的奇偶

還是給定一個8位的二進制,統計這8個數字中1個數的奇偶性,若1個數爲奇數,則返回1;若1個數爲偶數,則返回0。比如00110111,裏面一共有5個1,所以應該返回1。

這道題利用位運算的思路和上一道有些相似,也是利用按位與“同真則真,反之則假”的法則,循環遍歷這8個數字,用計數器count統計1的個數,最後用count&1判斷count的奇偶性即可,奇數返回1,偶數則返回0。

count = 0
for i in range(8):
    count += n & 1
    n >>= 1
return count & 1

但是這並不是位運算最NiuBi之處,下面這種方法纔是真的強,但是可讀性可能不如上面的方法。

n = n^(n>>1)
n = n^(n>>2)
n = n^(n>>4)
return n & 1

仍用00110111舉例,將n與n>>1做異或操作,得到一個新的二進制,具體如下:

在這裏插入圖片描述

新二進制中0的意義表示該位置與前一個位置共有偶數個1,1則表示該位置與前一個位置共有奇數個1。比如New中第6位的1表示Num1中第5位和第6位共有奇數個1,可以看到Num1中對應位置爲01是符合的,同理可以對比一下其他位置也是具有這個性質。

在這裏插入圖片描述

同理移動2位表示該位置與前三個位置1個數的奇偶性、移動4位表示該位置與前七個位置1個數的奇偶性,所以當移動4位後末位的數字就表示整個8位二進制中1的奇偶性,如果末位爲1,則代表有奇數個1,如果末位爲0,則代表有偶數個1。

總結

位運算可能很多人都知道,但是在解題的時候卻很少用到,因爲有很多通俗易懂的方法可以代替位運算,但是位運算的功能是非常很強大的,值得學習一下。通過上面例子也可以發現位運算解決某些問題真的是很便捷,但對不理解的人可讀性會比較差,同時這也是位運算可以ZhuangBi之處。

關注公衆號【奶糖貓】第一時間獲取更多精彩好文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章