↑ 點擊上方藍字" 奶糖貓 ",加個關注如何

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

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

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

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

  • 按位與:&

  • 按位或:|

  • 按位異或:^

  • 左移:<<

  • 右移:>>

  • 取反:~

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

1

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

forin range( 0 ,len(m)):

first ^= ord(m[i])

return

chr(first)

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

翻轉二進制

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

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

res =  0

forin 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

forin 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個,這次移動兩個)1個數的奇偶性移動 4位 表示該位置與前七個位置1個數的奇偶性 所以當移動4位後末位的數字就表示整個8位二進制中1的奇偶性 ,如果末位爲1,則代表有奇數個1;如果末位爲0,則代表有偶數個1。

上文例題部分來源於力扣(LeetCode)

總結

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

End

相關文章