超全的位運算介紹與總結

前言

位運算隱藏在編程語言的角落中,其神祕而又強大,暗藏內力,有些人光聽位運算的大名的心中忐忑,還有些人更是一看到位運算就遠遠離去,我以前也是。但狡猾的面試官每每喜歡搞偷襲,抓住咱們的弱點搞咱們,爲了防患於未然,特記此篇!java

本篇的內容爲位運算的介紹和一些比較經典的位運算問題進行介紹分析,固然,位運算這麼牛,後面確定仍是要概括總結的。git

認識位運算

什麼是位運算?github

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

位運算就是直接操做二進制數,那麼有哪些種類的位運算呢?面試

常見的運算符有與(&)、或(|)、異或(^)、取反(~)、左移(<<)、右移(>>是帶符號右移 >>>無符號右移動)。下面來細看看每一種位運算的規則。算法

位運算 & (與)編程

規則:二進制對應位兩兩進行邏輯AND運算(只有對應位的值都是 1 時結果才爲 1, 不然即爲 0)即 0&0=0, 0&1=0, 1&1=1數組

例如:2 & -2數據結構

image-20210103203435246

位運算 | (或)編程語言

規則:二進制對應位兩兩進行邏輯或運算(對應位中有一 個爲1則爲1) 即0|0=0,0|1=1,1|1=1函數

例如:2 | -2

image-20210103203852165

位運算 ^ (異或)

規則:二進制對應位兩兩進行邏輯XOR (異或) 的運算(當對應位的值不一樣時爲 1, 不然爲 0)即0^0=0, 0^1=1, 1^1=0

例如:2 ^ -2

image-20210103204258194

按位取反~

規則:二進制的0變成1,1變成0。

image-20210103204832085

移位運算符

左移運算<<:左移後右邊位補 0

右移運算>>:右移後左邊位補原最左位值(多是0,多是1)

右移運算>>>:右移後左邊位補 0

  • 對於左移運算符<<沒有懸念右側填個零不管正負數至關於整個數乘以2。
  • 而右移運算符就有分歧了,分別是左側補0>>>和左側補原始位>>,若是是正數沒爭議左側都是補0,達到除以2的效果;若是是負數的話左側補0>>>那麼數值的正負會發生改變,會從一個負數變成一個相對較大的正數。而若是是左側補原始位(負數補1)>>那麼整個數仍是負數,也就是至關於除以2的效果。

下面這張圖能夠很好的幫助你理解負數的移位運算符:

image-20210111203911045

到這裏,我想你應該對位運算有了初步的認識,在這裏把上面提到的部分案例執行對比一下讓你看一下可能會理解的更清晰:

image-20210112233233639

位運算小技巧

在這裏有些經常使用的位運算小技巧。

判斷奇偶數

正常判斷奇數偶數的時候咱們會這樣寫:

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來表示。而在實現上,經過枚舉數字範圍分析每一個二進制數字各符號位上的特徵進行計算求解操做便可。

image-20210122160614157

二進制枚舉的代碼實現爲:

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<<0=1 =00000000000000000000000000000001
1<<1=2 =00000000000000000000000000000010
1<<2=4 =00000000000000000000000000000100
1<<3=8 =00000000000000000000000000001000
. . . . . .
1<<30=2^30 =01000000000000000000000000000000
1<<31=-2^31 =10000000000000000000000000000000

其次還要知道位運算&與。兩個十進制與運算.每一位同1爲1。因此咱們用2的正數次冪與知道的數分別進行與運算操做。若是結果不爲0,那麼就說明這位爲1.(前面31個都是大於0的最後一個與結果是負數可是若是該位爲1那麼結果確定不爲0)

image-20210122170223380

具體代碼實現爲:

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。
image-20210122163518288
實現代碼爲:

public class Solution {
    public int NumberOf1(int n) {
       int count=0;
       while (n!=0) {
           n=n&(n-1);
           count++;
       }
       return count;
    }
}

只出現一次的(一個)數字①

問題描述:

給定一個非空整數數組,除了某個元素只出現一次之外, 其他每一個元素均出現兩次。找出那個只出現了一次的元素。

說明:你的算法應該具備線性時間複雜度。 你能夠不使用額外空間來實現嗎?

分析:

這是一道簡單的面試題,面試官常問怎麼樣用不太複雜的方法找出數組中僅出現一次的數字(其餘均出現兩次),暴力枚舉或者使用其餘的存儲結構都不夠優化,而這個問題最高效的答案就是使用位運算。首先你要注意兩點:

  • 0和任意數字進行異或操做結果爲數字自己.
  • 兩個相同的數字進行異或的結果爲0.

具體的操做就是用0開始和數組中每一個數進行異或,獲得的值和下個數進行異或,最終得到的值就是出現一次(奇數次)的值。

image-20210122002813297

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.

image-20210122122756549

在具體的操做實現上,問題中給出數組中的數據在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。

具體能夠參考下圖流程:

image-20210121225712670

實現代碼爲:

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;
}

結語

固然,上面的問題可能有更好的解法,也有更多經典位運算問題將在後面概括總結,但願本篇的位運算介紹可以讓你有所收穫,對位運算能有更深一點的認識。對於不少問題例如博弈問題等二進制位運算可以很巧妙的解決問題,往後也會分享相關內容,敬請期待!

原創公衆號: bigsai
原創不易,若是有收穫請不要吝嗇你的 贊贊
文章已收錄在 全網都在關注的數據結構與算法學習倉庫

我們下次再見!

相關文章
相關標籤/搜索