位運算的刷題應用

不申請臨時變量的整數交換

private static void swap(int a, int b) {
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
}
複製代碼

關於這樣作行之有效的緣由

先來解釋b 爲何會變成 a。算法

首先,異或運算是符合交換律和結合律的,就是說 a ^ b ^ c 等於 a ^ c ^ b 等於 (a ^ b) ^ c。編程

因此,第二條語句就能夠等價爲 : b = a ^ b ^ b; ,由於(b ^ b) = 0 ,因此 b = a ^ 0 = a。數組

再來解釋 a 爲何會變成 b。ui

結合以上說法,當運行到 第三行代碼時,a = a ^ b 而 b = a,因此 a = a ^ b = a ^ b ^ a。spa

同上,結合異或運算的交換律, a = (a ^ a) ^ b = b。code

其實,只要是互逆運算就能夠進行這種不用臨時變量的交換運算,如 加減法,乘除法。教程

public static void swap(int a,int b) {
    a = a + b;
    b = a - b;
    a = a - b;
}
複製代碼
public static void swap(int a,int b) {
    a = a * b;
    b = a / b;
    a = a / b;
}
複製代碼

不使用 + 來計算整數的加法(寫加法器)

public static int plus(int a, int b) {
    while (b != 0) 
    {
        int _a = a ^ b;
        int _b = (a & b) << 1;
        a = _a;
        b = _b;
    }
    return a;
}
複製代碼

關於這樣作行之有效的緣由

主要利用異或運算來完成,異或運算有一個別名叫作:不進位加法。ip

那麼a ^ b就是a和b相加以後,該進位的地方不進位的結果,相信這一點你們沒有疑問,可是須要注意的是,這個加法是在二進制下完成的加法。get

而後下面考慮哪些地方要進位?it

什麼地方須要進位呢? 天然是a和b裏都是1的地方

a & b就是a和b裏都是1的那些位置,那麼這些位置左邊都應該有一個進位1,a & b << 1 就是進位的數值(a & b的結果全部左移一位)。

那麼咱們把不進位的結果和進位的結果加起來,就是實際中a + b的和。

a + b = (a ^ b) + (a & b << 1)

令:

a' = a ^ b, b' = (a & b) << 1 => a + b = (a ^ b) + (a & b << 1) = a' + b'

而後反覆迭代,這個過程是在二進制下模擬加法的運算過程,進位不可能一直持續,因此b最終會變爲0,也就是沒有須要進位的了,所以重複作上述操做就能夠 最終求得a + b的值。

用 O(1) 時間檢測整數 n 是不是 2 的冪次

N若是是2的冪次,則N知足兩個條件。

N > 0

N的二進制表示中只有一個1, 注意只能有1個。

由於N的二進制表示中只有一個1,因此使用N & (N - 1)將N惟一的一個1消去,應該返回0。

bool checkPowerOf2(int n) {
    return n > 0 && (n & (n - 1)) == 0;
}
複製代碼

x & (x - 1)的妙用

x & (x - 1)消去x最後一位的1。

計算在一個 32 位的整數的二進制表式中有多少個 1

由x & (x - 1)消去x最後一位的1可知。不斷使用 x & (x - 1) 消去x最後一位的1,計算總共消去了多少次便可。

public int countOnes(int num) {
    int count = 0;
    while (num != 0) // 不能是 > 0,由於要考慮負數
    {
        num = num & (num - 1);
        count++;
    }
    return count;
}
複製代碼

若是要將整數A轉換爲B,須要改變多少個bit位?

這個應用是上面一個應用的拓展

思考將整數A轉換爲B,若是A和B在第i(0 <=i < 32)個位上相等,則不須要改變這個BIT位,若是在第i位上不相等,則須要改變這個BIT位。

因此問題轉化爲了A和B有多少個BIT位不相同!

聯想到位運算有一個異或操做,相同爲0,相異爲1,因此問題轉變成了計算A異或B以後這個數中1的個數!

public int countOnes(int num) {
    int count = 0;
    while (num != 0) // 不能是 > 0,由於要考慮負數
    {
        num = num & (num - 1);
        count++;
    }
    return count;
}

public int bitSwapRequired(int a, int b) {
    return countOnes(a ^ b);
}
複製代碼

應用:給定一個含不一樣整數的集合,返回其全部的子集

思路就是使用一個正整數二進制表示的第i位是1仍是0來表明集合的第i個數取或者不取。 因此從0到2^n-1總共2^n個整數,正好對應集合的2^n個子集。以下是就是 整數 <=> 二進制 <=> 對應集合 之間的轉換關係。

public List<List<Integer>> bitSubsets(int[] nums) 
{
    Arrays.sort(nums);
    List<List<Integer>> list = new ArrayList<>();
    int n = nums.length;
    for (int i = 0; i < (1 << n); ++i) {
        List<Integer> subset = new ArrayList<>();
        for (int j = 0; j < n; j++) {
            if((1 << j & i) != 0) {
                subset.add(nums[j]);
            }
        }
        list.add(subset);
    }
    return list;
}
複製代碼

巧用異或運算

應用一:數組中,只有一個數出現一次,剩下都出現兩次,找出出現一次的數

由於只有一個數剛好出現一個,剩下的都出現過兩次,因此只要將全部的數異或起來,就能夠獲得惟一的那個數,由於相同的數出現的兩次,異或兩次等價於沒有任何操做!

public int singleNumber(int[] nums) {
    int result = 0, n = nums.length;
    for (int i = 0; i < n; i++)
    {
        result ^= nums[i];
    }
    return result;
}
複製代碼

應用二:數組中,只有一個數出現一次,剩下都出現三次,找出出現一次的數

由於其餘數是出現三次的,也就是說,對於每個二進制位,若是隻出現一次的數在該二進制位爲1,那麼這個二進制位在所有數字中出現次數沒法被3整除。

對於每一位,咱們讓Two,One表示當前位的狀態。

咱們看Two和One裏面的每一位的定義,對於ith(表示第i位):

若是Two裏面ith是1,則ith當前爲止出現1的次數模3的結果是2

若是One裏面ith是1,則ith目前爲止出現1的次數模3的結果是1

注意Two和One裏面不可能ith同時爲1,由於這樣就是3次,每出現3次咱們就能夠抹去(消去)。那麼最後One裏面存儲的就是每一位模3是1的那些位,綜合起來One也就是最後咱們要的結果。

若是B表示輸入數字的對應位,Two+和One+表示更新後的狀態

那麼新來的一個數B,此時跟原來出現1次的位作一個異或運算,&上~Two的結果(也就是否是出現2次的),那麼剩餘的就是當前狀態是1的結果。

同理Two ^ B (2次加1次是3次,也就是Two裏面ith是1,B裏面ith也是1,那麼ith應該是出現了3次,此時就能夠消去,設置爲0),咱們至關於會消去出現3次的位。

可是Two ^ B也多是ith上Two是0,B的ith是1,這樣Two裏面就混入了模3是1的那些位!!!怎麼辦?咱們得消去這些!咱們只須要保留不是出現模3餘1的那些位ith,而One是剛好保留了那些模3餘1次數的位,`取反不就是否是模3餘1的那些位ith麼?最終對(~One+)取一個&便可。

public int singleNumber(int[] nums) {
    int ones = 0, twos = 0;
    for(int i = 0; i < nums.length; i++)
    {
        ones = (ones ^ nums[i]) & ~twos;
        twos = (twos ^ nums[i]) & ~ones;
    }
    return ones;
}
複製代碼

應用三:數組中,只有兩個數出現一次,剩下都出現兩次,找出出現一次的這兩個數

有了第一題的基本的思路,咱們能夠將數組分紅兩個部分,每一個部分裏只有一個元素出現一次,其他元素都出現兩次。那麼使用這種方法就能夠找出這兩個元素了。不妨假設出現一個的兩個元素是x,y,那麼最終全部的元素異或的結果就是等價於++res = x^y++。

++而且res!=0++

爲何呢? 若是res 等於0,則說明x和y相等了!!!!

由於res不等於0,那麼咱們能夠必定能夠找出res二進制表示中的某一位是1。

對於x和y,必定是其中一個這一位是1,另外一個這一位不是1!!!細細琢磨, 由於若是都是0或者都是1,怎麼可能異或出1

對於原來的數組,咱們能夠根據這個位置是否是1就能夠將數組分紅兩個部分。++x,y必定在不一樣的兩個子集中。++

並且對於其餘成對出現的元素,要麼都在x所在的那個集合,要麼在y所在的那個集合。對於這兩個集合咱們分別求出單個出現的x 和 單個出現的y便可。

public int[] singleNumber(int[] nums) {
    //用於記錄,區分「兩個」數組
    int diff = 0;
    for(int i = 0; i < nums.length; i ++) 
    {
        diff ^= nums[i];
    }
    //取最後一位1
    //先介紹一下原碼,反碼和補碼
    //原碼,就是其二進制表示(注意,有一位符號位)
    //反碼,正數的反碼就是原碼,負數的反碼是符號位不變,其他位取反
    //補碼,正數的補碼就是原碼,負數的補碼是反碼+1
    //在機器中都是採用補碼形式存
    //diff & (-diff)就是取diff的最後一位1的位置
    diff &= -diff;
    
    int[] rets = {0, 0}; 
    for(int i = 0; i < nums.length; i ++) 
    {
        //分屬兩個「不一樣」的數組
        if ((nums[i] & diff) == 0) 
        {
            rets[0] ^= nums[i];
        }
        else 
        {
            rets[1] ^= nums[i];
        }
    }
    return rets;
}
複製代碼

最後

位運算的妙用到這就告一段落了,以上內容有一大部分是來自 九章算法位運算入門教程 , 在 LintCode 上還有相應的編程練習,去練習一下會得到更好的效果。

相關文章
相關標籤/搜索