1. 程式人生 > >位運算知識點總結

位運算知識點總結

一,位運算的操作符

        在C/C++中與位運算有關的操作符並不多,一共只有6個,分別是按位與(&)、按位或(|)、按位非(~)、按位異或(^)、左移(<<)與右移操作(>>)。

1、與運算(A & B)

        對應的位上均為1時,運算結果為1,否則為0。

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int A = 0xb3;//1011 0011
    int B = 0x6a;//0110 1010
    
    printf("%#x",A & B);//輸出0x22 對應的二進位制位:0010 0010

    return 0;
}

2、或運算(A | B)

對應的位上至少有一個1時,運算結果為1,否則為0。

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int A = 0xb3;//1011 0011
    int B = 0x6a;//0110 1010

    printf("%#x",A | B);//輸出0xfb 對應的二進位制位:1111 1011

    return 0;
}

3、非運算(~A)

單目操作符,原來的位上是1,操作後變為0;原來的位上是0,操作後變為1。

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int A = 0x000000b3;//0000 0000 0000 0000 0000 0000 0000 0000 1011 0011

    printf("%#x",~A);//輸出0xffffff4c 對應的二進位制位:1111 1111 1111 1111 1111 1111 0100 1100

    return 0;
}

4、異或運算(A ^ B)

當且僅當對應的位上一個是0,一個是1時,結果為1,否則為0。

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int A = 0xb3;//1011 0011
    int B = 0x6a;//0110 1010

    printf("%#x",A ^ B);//輸出0xd9 對應的二進位制位:1101 1001

    return 0;
}

異或運算需要注意的三個使用技巧:1、一個數與其自身相異或結果0。2、一個數與0相異或的結果還是自身。3、一個數與二進位制數全是1的數(-1)異或的結果是其反碼,等同於非操作。

5、左移運算(A << B)

將A的二進位制表示式左移B位,右邊空出的位補0。(如果B大於32的話,會自動進行模32操作。也就是說,左移32位與左移0位是相等的;左移33位與左移1位是相等的.....)

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int A = 0xb3;//1011 0011

    printf("%#x\n\n",A << 2);//輸出0x2cc 對應的二進位制位:0010 1100 1100

    printf("%#x\n",A << 32);// 輸出0xb3 對應的二進位制位:1011 0011
    printf("%#x\n\n",A << 0);//輸出0xb3 對應的二進位制位:1011 0011

    printf("%#x\n",A << 33);// 輸出0x166 對應的二進位制位:0001 0110 0110
    printf("%#x\n\n",A << 1);//輸出0x166 對應的二進位制位:0001 0110 0110

    return 0;
}

6、右移操作(A >> B)

與左移操作一樣,將A的二進位制表示形式右移B位。但是右移操作分為兩種,一種是邏輯右移,一種是算術右移。

邏輯右移:右移後最高位補0。

算術右移:右移後最高位補符號位。

C/C++中邏輯右移與算術右移是共享一個操作符的(>>),而java或者python中使用(>>)表示算術右移,(>>>)表示邏輯右移。那麼在C/C++中什麼時候表示表示邏輯右移,什麼時候表示算術右移呢?如果運算數型別是unsigned則採用邏輯右移,而signed則採用算數右移。對於signed型別的資料,如果需要使用算數右移,或者unsigned型別的資料需要使用邏輯右移,都需要進行型別轉換。(引用C/C++中的邏輯右移、算數右移、迴圈左移、迴圈右移

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int A = 0xb3;//1011 0011

    printf("%#x\n",A >> 2);//算術右移,最高位補符號位0 輸出0x2c 對應的二進位制位:0010 1100
    printf("%#x\n\n",(unsigned int)A >> 2);//邏輯右移,最高位補0 輸出0x2c 對應的二進位制位:0010 1100

    
    A = -1;//1111 1111 1111 1111 1111 1111 1111 1111

    printf("%#x\n",A >> 2);//算術右移,最高位補符號位1 輸出0xffffffff 對應二進位制位:1111 1111 1111 1111 1111 1111 1111 1111
    printf("%#x\n\n",(unsigned int)A >> 2);//邏輯右移,最高位補0 輸出0x3fffffff 對應二進位制位:0011 1111 1111 1111 1111 1111 1111 1111

    return 0;
}

注:一個數左移n位,相當於乘2的n次方;一個數右移n位,相當於除2的n次方。

二、小試牛刀,位運算的一些小技巧。

1、從低位到高位,只保留第一個遇到的1,其餘的全置0(0110 1110 --> 0000 0010)

int fun1(int num)
{
    return num & (-num);
}

解釋:這裡用到了正數、負數在計算機裡儲存時的編碼機制。正數的儲存使用的是原碼,而負數使用的是補碼。補碼 = 原碼取反 + 1,這樣num 與 -num在計算機中的編碼表示只有在最低位1處均為1,比該位高的位均相反,比該位低的位均為0。(如num的二進位制表示為 1010 那麼-num的二進位制位0110 所以 num & (-num) 為 0010)。

應用1:計算一個整數二進位制表示式中1的個數(見後)

應用2:判斷一個數是否是2的冪次方。分析:我們知道,如果一個數是2的冪次方,那麼該數的二進位制表示式只有一個1,其餘的都是0,所以 num & (-num) = 0。

#include <bits/stdc++.h>

using namespace std;
   
 bool isPowerOfTwo(int num) {
     return (num > 0 && (num & (num-1)) == 0);
 }

2、從高位到低位,只保留第一個遇到的1,其餘的全置0(0110 1110 --> 0100 0000)

int fun2(int num)
{
    num |= (num >> 1);
    num |= (num >> 2);
    num |= (num >> 4);
    num |= (num >> 8);
    num |= (num >> 16);

    return num - ((unsigned int)num >> 1);
}

解釋:自己用筆畫一畫就可以了,最後一個地方需要使用邏輯右移,所以強制型別轉換了。

3、從低位到高位,將遇見的第一個1置0,其餘的不變(0110 1110 --> 0110 1100)

int fun3(int num)
{
    return num & (num-1)
}

擴充套件:如果是從高位到低位呢? return func2(num)  ^  num;

4、判斷兩個數是否異號

bool fun4(int num1,int num2)
{
    return (num1 ^ num2) < 0;//異號返回true,同號返回false
}

解釋:若兩數異號,則其符號位一個為0,一個為1,異或之後為1,所以結果小於0。 

5、判斷奇偶性

bool func5(int num)
{
    if(num&1 == 1)
        return false;//奇數
    return true;//偶數
}

6、不使用第三個變數交換兩個數

void func6(int &num1,int &num2)
{
    num1 = num1 ^ num2;
    num2 = num1 ^ num2;
    num1 = num1 ^ num2;   
}

void func7(int &num1,int &num2)
{
    num1 = num1 + num2;
    num2 = num1 - num2;
    num1 = num1 - num2;
}

7、一點好玩的東西

    1.使用位運算計算 n + 1

int bitAddOne(int n)
{
    return -~n;
}

解釋:由原始碼與補碼的關係可知:-n = ~n + 1   移項得  -~n = n+1

    2.使用位運算計算 n-1

int bitMinusOne(int n)
{
    return ~-n;
}

解釋:由原始碼與補碼的關係可知:n = ~-n + 1   移項得  ~-n = n-1

三、位運算的一些應用題

1、計算一個整數二進位制表達形式中1的個數

分析:1、將該數與1進行與(&)運算,如果是0,說明最後一位為0 ,如果是1,說明最後一位為1,。依次將該數邏輯右移(什麼是邏輯右移,以及在C/C++中怎麼實現邏輯右移在上面已經說明了。---就是強制型別轉換)或者將1依次進行左移操作,放回最後的結果即可。2、在1中,我們判斷的次數和二進位制表示式的位數相同,因為不管該位上是0還是1都需要判斷,在上面我們知道 num & (num - 1) 會將num進製表達式中最低位的1清0,這樣只要num不為0我們就執行 num &= num-1 操作,執行的次數與該數1的個數相等。

#include<bits/stdc++.h>

using namespace std;

int numberOf1(int num)
{
    int res = 0;
    while(num != 0)
    {
        res++;
        num &= num-1;
    }

    return res;
}

2、給你一個非空的整數陣列,裡面只有一個數只出現了一次,其餘的數都出現了兩次,輸出這個這出現一次的數字。

分析:在前面介紹異或的時候提到過,一個數字與其自身進行異或得0;一個數字與0異或還是自身。因此我們將陣列中所有的數都異或一遍最終得到的就是我們想要的結果。時間複雜度o(n),空間複雜度o(1)

#include<bits/stdc++.h>

using namespace std;

int singleNumber(const vector<int> & nums)
{
    int res = 0;
    for(auto num : nums)
        res ^= num;
    
    return res;
}

擴充套件1:如果給你的陣列是 只有一個數字出現了1次,其餘的數字均出現了3次,該怎麼解決。

這裡先給出最優解法

#include<bits/stdc++.h>

using namespace std;

int singleNumber(const vector<int> & nums)
{
    int a = 0,b = 0;
    for(int i = 0; i < nums.size(); ++i)
    {
        a = (a ^ nums[i]) & (~b);
        b = (b ^ nums[i]) & (~a);
    }
        
    return a;
}

解釋:第一次看到時肯定有點暈,要想不暈最簡單的辦法就是找幾個數執行以下就明白了。

*    初始:a=0,b=0 *    x第一次出現時  a=(a^x)&(~b) = x,b=(b^x)&(~a) = 0 *    第一次後:a=x,b=0 *    x第二次出現時  a=(a^x)&(~b) = 0,b=(b^x)&(~a) = x *    第二次後:a=0,b=x *    x第三次出現時  a=(a^x)&(~b) = 0,b=(b^x)&(~a) = 0 *    第三次後:a=0,b=0 回到了初始狀態。 *     *    因此當x第一次出現時,其值保留在a中;第二次出現時,其值被保留到了b中;第三次出現時,a和b都被清0了,回到了初始狀態。

上述解法的確優秀,執行速度也很快,但是很難想到,有沒有通用的解法呢?可以解決這一類問題,無論其餘的數是出現了2次、3次....還是n次。

通用解法:

        宣告一個size為32的陣列,每一維度用來記錄出現在該位置處1的個數,再對n取模,這樣那些出現n次的數都被清除了,最後再拼裝成最後的結果即可。

#include<bits/stdc++.h>

using namespace std;

int singleNumber(const vector<int>& nums) {
    vector<int> countBit(32,0);
    for(int i = 0;i < 32;++i)
    {
        for(int j = 0;j < (int)nums.size();++j)
        {
            countBit[i] += (nums[j] >> i) & 1;
        }
    }
    int res = 0;
    for(int i = 0; i < 32;++i)
    {
        countBit[i] %= 3;
        res += countBit[i] << i;
    }

    return res;
}

擴充套件2:還是給你一個數組,其中有兩個數只出現一個一次,其餘的數字均出現兩次,返回只出現一次的兩個數。

分析:我們知道:1、如果所有的數字均出現兩次,那麼將陣列中每個數字異或的結果為0。2、如果只有一個數字出現一次,其餘的數字均出現兩次,那麼將陣列中每個數字異或的結果正好為那個只出現一次的數字。3、如果有兩個不同的數出現一次,其餘的數字均出現兩次,那麼將陣列中每個數字異或的結果為那兩個只出現一次的數字的異或值,這個數肯定不為0,除非這兩個數相等。那麼我們可以將陣列分成兩組,每一組中只含有一個數字出現一次,其餘數字出現兩次。這樣講這兩組數字分別進行異或操作就可以得到最終的結果了。

        怎麼分呢?在3中我們得到了這兩個數的異或值,並且也確定這個數不為0,假如該數的二進位制表示式中第x位為1,說明這兩個數在x位出一個為0,一個為1,所以他們的異或值為1。那麼我們就可以根據一個數在x位是否為1,將其劃分成兩個組。

#include <bits/stdc++.h>

using namespace std;

vector<int> singleNumber(const vector<int>& nums) {
    if(nums.size() == 2)
        return vector<int>{nums[0],nums[1]};
    int res = 0;  //res儲存的是兩個相異值的異或值
    for(const auto & num : nums)
        res = res ^ num;
    //取得res最後一位不為0
    int tmp = res & (-res);

    int a=0,b=0;
    for(int i = 0;i < (int)nums.size();++i)
    {
        if(nums[i] & tmp != 0)
            a ^= nums[i];
        else
            b ^= nums[i];
    }
    return vector<int>{a,b};
}

注:res & (-res) 的結果是隻保留res最低位的1,其餘的全置0。

3、給你一個數組和一個目標值,判斷該陣列中是否存在若干個數的邏輯或等於目標值。

分析:題目意思很簡單,就是在一個數組中選出一些數出來,它們的邏輯或等於目標值。首先,要確定哪些數應該篩選出來(在target的二進位制表示式為0的地方也為0的數可以被篩選出來),將篩選出來的數進行或操作如果等於target就返回true,否則返回false。

#include <bits/stdc++.h>

using namespace std;

bool numsOrEqualTarget(const vector<int> & nums,int target)
{
    int num = 0;
    for(int i = 0;i < (int)nums.size();++i)
    {
        if((nums[i] & (~target)) == 0)
        {
            num |= nums[i];
            if(num == target)
                return true;
        }
    }

    return false;
}