爲什麼程序員必須懂點位運算
前 言
位運算隱藏在編程語言的角落中,其神祕而又強大,暗藏內力,有些人光聽位運算的大名的心中忐忑,還有些人更是一看到位運算就遠遠離去。但狡猾的面試官往往喜歡搞偷襲,抓住我們的弱點搞我們,爲了防患於未然,有了這篇!
本篇的內容爲位運算的介紹和一些比較經典的位運算問題的分析,當然,位運算這麼牛,後面肯定還是要歸納總結的。
認識位運算
什麼是位運算?
程序中的所有數在計算機內存中都是以二進制的形式儲存的。位運算就是直接對整數在內存中的二進制位進行操作。
位運算就是直接操作二進制數,那麼有哪些種類的位運算呢?
常見的運算符有與 (&)、或(|)、異或(^)、取反(~)、左移(<<)、右移(>> 是帶符號右移 >>>無符號右移動)。下面來細看看每一種位運算的規則。
位運算 & (與)
規則:二進制對應位兩兩進行邏輯 AND 運算 (只有對應位的值都是 1 時結果才爲 1, 否則即爲 0) 即 0&0=0
,0&1=0
,1&1=1
例如:2 & -2
位運算 | (或)
規則:二進制對應位兩兩進行邏輯或運算 (對應位中有一 個爲 1 則爲 1) 即0|0=0
,0|1=1
,1|1=1
例如:2 | -2
位運算 ^ (異或)
規則:二進制對應位兩兩進行邏輯 XOR (異或) 的運算 (當對應位的值不同時爲 1, 否則爲 0) 即0^0=0
, 0^1=1
, 1^1=0
例如:2 ^ -2
按位取反~
規則:二進制的 0 變成 1,1 變成 0。
移位運算符
左移運算<<
:左移後右邊位補 0
右移運算>>
:右移後左邊位補原最左位值 (可能是 0,可能是 1)
右移運算>>>
:右移後左邊位補 0
-
對於左移運算符
<<
沒有懸念右側填個零無論正負數相當於整個數乘以 2。 -
而右移運算符就有分歧了,分別是左側補 0
>>>
和左側補原始位>>
,如果是正數沒爭議左側都是補 0,達到除以 2 的效果;如果是負數的話左側補 0>>>
那麼數值的正負會發生改變,會從一個負數變成一個相對較大的正數。而如果是左側補原始位 (負數補 1)>>
那麼整個數還是負數,也就是相當於除以 2 的效果。
下面這張圖可以很好的幫助你理解負數的移位運算符:
到這裏,我想你應該對位運算有了初步的認識,在這裏把上面提到的部分案例執行對比一下讓你看一下可能會理解的更清晰:
位運算小技巧
在這裏有些常用的位運算小技巧。
判斷奇偶數
正常判斷奇數偶數的時候我們會這樣寫:
if( n % 2 == 1)
// n 是個奇數
}
使用位運算可以這麼寫:
if(n & 1 == 1){
// n 是個奇數。
}
其核心就是判斷二進制的最後一位是否爲 1,如果爲 1 那麼結果加上 2^0=1 一定是個奇數,否則就是個偶數。
交換兩個數
對於傳統的交換兩個數,我們需要使用一個變量來輔助完成操作,可能會是這樣:
int team = a;
a = b;
b = team;
但是使用位運算可以不需要藉助額外空間完成數值交換:
a=a^b;//a=a^b
b=a^b;//b=(a^b)^b=a^0=a
a=a^b;//a=(a^b)^(a^b^b)=0^b=0
原理已經寫在註釋裏面了,是不是感覺非常 diao 呢?
二進制枚舉
在遇到子集問題的處理時候,我們有時候會藉助二進制枚舉來遍歷各種狀態 (效率大於 dfs 回溯)。這種就屬於排列組合的問題了,對於每個物品(位置) 來說,就是使用和不使用的兩個狀態,而在二進制中剛好可以用 1 和 0 來表示。而在實現上,通過枚舉數字範圍分析每個二進制數字各符號位上的特徵進行計算求解操作即可。
二進制枚舉的代碼實現爲:
for(int i = 0; i < (1<<n); i++) //從0~2^n-1個狀態
{
for(int j = 0; j < n; j++) //遍歷二進制的每一位 共n位
{
if(i & (1 << j))//判斷二進制數字i的第j位是否存在
{
//操作或者輸出
}
}
}
位運算經典問題
有了上面的位運算基礎,我們怎麼用位運算處理實際問題呢?或者有哪些經典的問題可以用位運算來解決呢。
不用加減乘除做加法
題目描述
寫一個函數,求兩個整數之和,要求在函數體內不得使用 +、-、*、/ 四則運算符號。
分析:這道題咋一聽可能沒啥思路,簡單研究一下位運算還是能獨立推出來和理解的。
當然,解決這題前,需要了解上面的四種位運算。還要知道二進制的運算:0+0=0,0+1=1,1+1=0(進位)
對於加法的一個二進制運算。如果不進位那麼就是非常容易的。這時候相同位都爲 0 則爲 0,0 和 1 則爲 1. 滿足這種運算的異或 (不相同取 1,相同取 0) 和或(有一個 1 則爲 1) 都能滿足.
-
用兩個數,一個正常 m 相加 (不考慮進位的)。用異或 a^b 就是滿足這種要求,先不考慮進位 (如果沒進位那麼就是最終結果)。另一個專門考慮進位的 n。兩個 1 需要進位。所以我們用 a&b 與記錄需要進位的。但是還有個問題,進位的要往上面進位,所以就變成這個需要進位的數左移一位。
-
然後就變成 m+n 重新迭代開始上面直到不需要進位的 (即 n=0 時候)。
實現代碼爲:
public class Solution {
public int Add(int num1,int num2) {
/*
* 5+3 5^3(0110) 5&3(0001)
* 0101
* 0011
*/
int a=num1^num2;
int b=num1&num2;
b=b<<1;
if(b==0)return a;
else {
return Add(a, b);
}
}
}
當然,這裏也可以科普一下二進制求加法:average = (a&b) + ((a^b)>>1) ;
二進制中 1 的個數
這是一道經典題,在劍指 offer 上也有對應題目,其具體題目要求輸入一個整數,輸出該數二進制表示中 1 的個數 (其中負數用補碼錶示)。
對於這個問題,不用位運算將它轉成二進制字符串直接枚舉字符'1'的個數也可以直接求出來,但是這樣做是沒有靈魂的並且效率比較差。這裏提供兩種解決思路
法一:大家知道每個類型的數據它的背後實際都是二進制操作。大家知道 int 的數據類型的範圍是 (-2^31,2^31 -1)。並且 int 有 32 位。但是並非 32 位全部用來表示數據。真正用來表示數據大小的也是 31 位。最高位用來表示正負。
首先要知道:
其次還要知道位運算&
與。兩個十進制與運算. 每一位同 1 爲 1。所以我們用 2 的正數次冪與知道的數分別進行與運算操作。如果結果不爲 0,那麼就說明這位爲 1.(前面 31 個都是大於 0 的最後一個與結果是負數但是如果該位爲 1 那麼結果肯定不爲 0)
具體代碼實現爲:
public int NumberOf1(int n) {
int va=0;
for(int i=0;i<32;i++)
{
if((n&(1<<i))!=0)
{
va++;
}
}
return va;
}
法二是運用n&(n-1)
。n 如果不爲 0,那麼n-1
就是二進制第一個爲 1 的變爲 0,後面全爲 1. 這樣的n&(n-1)
一次運算就相當於把最後一個 1 變成 0. 這樣一直到運算的數爲 0 停止計算次數就好了,如下圖共進行三次運算那麼 n 的二進制中就有三個 1。
public class Solution {
public int NumberOf1(int n) {
int count=0;
while (n!=0) {
n=n&(n-1);
count++;
}
return count;
}
}
只出現一次的 (一個) 數字①
問題描述:
給定一個非空整數數組,除了某個元素只出現一次以外,其餘每個元素均出現兩次。找出那個只出現了一次的元素。
說明:你的算法應該具有線性時間複雜度。你可以不使用額外空間來實現嗎?
分析:
這是一道簡單的面試題,面試官常問怎麼樣用不太複雜的方法找出數組中僅出現一次的數字 (其他均出現兩次),暴力枚舉或者使用其他的存儲結構都不夠優化,而這個問題最高效的答案就是使用位運算。首先你要注意兩點:
-
0 和任意數字進行異或操作結果爲數字本身.
-
兩個相同的數字進行異或的結果爲 0.
具體的操作就是用 0 開始和數組中每個數進行異或,得到的值和下個數進行異或,最終獲得的值就是出現一次 (奇數次) 的值。
class Solution {
public int singleNumber(int[] nums) {
int value=0;
for(int i=0;i<nums.length;i++)
{
value^=nums[i];
}
return value;
}
}
只出現一次的 (一個) 數字②
問題描述:
給定一個非空整數數組,除了某個元素只出現一次以外,其餘每個元素均出現了三次。找出那個只出現了一次的元素。
說明:你的算法應該具有線性時間複雜度。你可以不使用額外空間來實現嗎?
分析:
這題和上一題的思路略有不同,這題其他數字出現了 3 次,那麼我們如果直接使用位運算異或操作的話無法直接找到結果,就需要巧妙的運用二進制的其他特性:判斷整除求餘操作。即判斷所有數字二進制 1 的總個數和 0 的總個數一定有一個不是三的整數倍,如果 0 不是三的整數倍那麼就說明結果的該位二進制數字爲 0,同理否則爲 1.
在具體的操作實現上,問題中給出數組中的數據在 int 範圍之內,那麼我們就可以在實現上可以對 int 的 32 個位每個位進行依次判斷該位 1 的個數求餘 3 後是否爲 1,如果爲 1 說明結果該位二進制爲 1 可以將結果加上去。最終得到的值即爲答案。
具體代碼爲:
class Solution {
public int singleNumber(int[] nums) {
int value=0;
for(int i=0;i<32;i++)
{
int sum=0;
for(int num:nums)
{
if(((num>>i)&1)==1)
{
sum++;
}
}
if(sum%3==1)
value+=(1<<i);
}
return value;
}
}
只出現一次的 (兩個) 數字③
題目描述
一個整型數組裏除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。
思路:
上面的問題處理和理解起來可能比較容易,但是這個問題可能稍微複雜一點,但是這題可以通過特殊的手段轉化爲上面只出現一次的一個數字問題來解決,當然核心的位運算也是異或^
。
具體思路就是想辦法將數組邏輯上一分爲二!先異或一遍到最後得到一個數,得到的肯定是a^b
(假設兩個數值分別爲 a 和 b) 的值。在看異或^
的屬性:不同爲 1,相同爲 0. 也就是說最終這個結果的二進制爲 1 的位置上 a 和 b 是不相同的。而我們可以找到這個第一個不同的位,然後將數組中的數分成兩份,該位爲 0 的進行異或求解得到其中一個結果 a,該位爲 1 的進行異或求解得到另一個結果 b。
具體可以參考下圖流程:
實現代碼爲:
public int[] singleNumbers(int[] nums) {
int value[]=new int[2];
if(nums.length==2)
return nums;
int val=0;//異或求的值
for(int i=0;i<nums.length;i++)
{
val^=nums[i];
}
int index=getFirst1(val);
int num1=0,num2=0;
for(int i=0;i<nums.length;i++)
{
if(((nums[i]>>index)&1)==0)//如果這個數第index爲0 和num1異或
num1^=nums[i];
else//否則和 num2 異或
num2^=nums[i];
}
value[0]=num1;
value[1]=num2;
return value;
}
private int getFirst1(int val) {
int index=0;
while (((val&1)==0&&index<32))
{
val>>=1;// val=val/2
index++;
}
return index;
}
結語
希望本篇的位運算介紹能夠讓我們有所收穫,對位運算能有更深一點的認識。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YFpR9JMhwQ4bGYJ9KPe5Yg