【計算機】位運算及其在算法中的應用


本文主要的理論部分轉載自:五色風車 位運算(&、|、^、~、>>、<<)
算法應用部分收集自: leetcode和部分博客。

一、位運算概述

探討位運算之前可以先了解了解這篇文章:深入理解原碼、反碼、補碼
常見的位運算包括以下幾種類型:

符號 含義 運算規則
& 兩個位都爲1時,結果才爲1
I 兩個位都爲0時,結果才爲0
^ 異或 兩個位相同時爲0,相異時爲1
~ 取反 1取反爲0,0取反爲1
<< 左移 各二進位全部左移若干位,高位丟棄,低位補0
>> 右移 各二進位全部右移若干位。無符號數,高位補0;;有符號數高位補1

舉個例子:
int a = 35; int b = 47; int c = a + b;
計算兩個數的和,因爲在計算機中都是以二進制來進行運算,所以上面我們所給的int變量會在機器內部先轉換爲二進制在進行相加

35: 0 0 1 0 0 0 1 1
47: 0 0 1 0 1 1 1 1
—————————
82: 0 1 0 1 0 0 1 0
相比在代碼中直接使用(+、-、*、/)運算符,合理的運用位運算更能顯著提高代碼在機器上的執行效率

二、位運算及其用途

1. 按位與運算符(&)

定義:參加運算的兩個數據,按二進制位進行“與”運算。

運算規則:兩位同時爲1,結果才爲1,否則結果爲0。

例如:3&5 即 0000 0011& 0000 0101 = 0000 0001,因此 3&5 的值得1。

注意負數按補碼形式參加按位與運算。

與運算的應用

1)取一個數的指定位

比如取數 X=1010 1110 的低4位,只需要另找一個數Y,令Y的低4位爲1,其餘位爲0,即Y=0000 1111,然後將X與Y進行按位與運算(X&Y=0000 1110)即可得到X的指定位。

2)判斷奇偶

只要根據最未位是0還是1來決定,爲0就是偶數,爲1就是奇數。
因此可以用if ((a & 1) == 0)代替if (a % 2 == 0)來判斷a是不是偶數

2.按位或運算符(|)

定義:參加運算的兩個對象,按二進制位進行“或”運算。

運算規則:參加運算的兩個對象只要有一個爲1,其值爲1。

例如:3|5即 0000 0011| 0000 0101 = 0000 0111,因此,3|5的值得7。

注意負數按補碼形式參加按位或運算。

或運算的應用

1)常用來對一個數據的某些位設置爲1

比如將數 X=1010 1110 的低4位設置爲1,只需要另找一個數Y,令Y的低4位爲1,其餘位爲0,即Y=0000 1111,然後將X與Y進行按位或運算(X|Y=1010 1111)即可得到。

3.異或運算符(^)

定義:參加運算的兩個數據,按二進制位進行“異或”運算。

運算規則:參加運算的兩個對象,如果兩個相應位相同爲0,相異爲1。

異或的幾條性質:

1、交換律

2、結合律 (a^ b)^ c == a^ (b^ c)

3、對於任何數x,都有 x^ x=0,x^ 0=x

4、自反性: a^ b^ b=a^ 0=a;

異或運算的應用

1)翻轉指定位

比如將數 X=1010 1110 的低4位進行翻轉,只需要另找一個數Y,令Y的低4位爲1,其餘位爲0,即Y=0000 1111,然後將X與Y進行異或運算(X^Y=1010 0001)即可得到。

2)與0相異或,值不變

例如:1010 1110 ^ 0000 0000 = 1010 1110
3)與自身相異或,值爲0

4)交換兩個數

void Swap(int &a, int &b){
    if (a != b){
        a ^= b;
        b ^= a;
        a ^= b;
    }
}

4.取反運算符 (~)

定義:參加運算的一個數據,按二進制進行“取反”運算。
運算規則:對一個二進制數按位取反,即將0變1,1變0。
取反運算的用途
1)使一個數的最低位爲零
使a的最低位爲0,可以表示爲:a & ~ 1。
~1的值爲 1111 1111 1111 1110,再按"與"運算,最低位一定爲0。

注意:“ ~”運算符的優先級比算術運算符、關係運算符、邏輯運算符和其他運算符都高。

5.左移運算符(<<)

定義:將一個運算對象的各二進制位全部左移若干位(左邊的二進制位丟棄,右邊補0)。

設 a=1010 1110,a = a<< 2 將a的二進制位左移2位、右補0,即得a=1011 1000。

若左移時捨棄的高位不包含1,則每左移一位,相當於該數乘以2。

6.右移運算符(>>)

定義:將一個數的各二進制位全部右移若干位,正數左補0,負數左補1,右邊丟棄。

例如:a=a>>2 將a的二進制位右移2位,左補0 或者 左補1得看被移數是正還是負。

操作數每右移一位,相當於該數除以2。

7.複合賦值運算符

位運算符與賦值運算符結合,組成新的複合賦值運算符,它們是:

&= 例:a&=b 相當於 a=a&b

|= 例:a|=b 相當於 a=a|b

>>= 例:a>>=b 相當於 a=a>>b

<<= 例:a<<=b 相當於 a=a<<b

^= 例:a^=b 相當於 a=a^b

運算規則:和前面講的複合賦值運算符的運算規則相似。

三、位運算相關的算法題解

例1:判斷一個正整數 n 是否爲 2 的冪次方

如果一個數是 2 的冪次方,意味着 n 的二進制表示中,只有一個位置 是1,其他都是0。舉個例子:

2^0 = 0……0001

2^1 = 0……0010

2^2 = 0……0100

爲了確定是否只有一個1,我們可以利用n & (n - 1)消去 n 最後的一位 1

在 n 的二進制表示中,如果我們對 n 執行n = n & (n - 1),例如:

n = 1001
n - 1 = 1000
n = n & (n - 1) = (1001) & (1000) = 1000

因此我們可以對 n 執行 n = n & (n - 1),執行之後結果如果不爲 0,則代表 n 不是 2 的冪次方,代碼如下:

bool judge(int n){
     return n & (n - 1) == 0;
}

例2:判斷 正整數 n 的二進制表示中有多少個 1

例如 n = 13,那麼二進制表示爲 n = 1101,那麼就表示有 3 個1

常規做法:還是把 n 不停着除以 2,然後統計除後的結果是否爲奇數,是則 1 的個數加 1,否則不需要加1,繼續除以 2。

另外的一種方法:類似例1,我們可以用不斷着執行 n & (n - 1),每執行一次就可以消去一個 1,當 n 爲 0 時,計算總共執行了多少次即可,代碼如下:

   int NumberOf12(int n) {
        int count = 0;
        int k = 1;
        while (n != 0) {
            count++;
            n = (n - 1) & n;
        }
        return count;
    }

例3:找出一個只出現一次的數

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

利用性質:兩個相同的數異或的結果是 0,一個數和 0 異或的結果是它本身。
所以我們可以把一組整型全部異或一下找出只出現一次的數字。例如這組數據是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出現了一次,其他都出現了兩次,把他們全部異或一下,結果如下:

由於異或支持交換律和結合律,所以:

1^ 2^ 3^ 4^ 5^ 1^ 2^ 3^ 4 = (1^ 1)^ (2^ 2)^ (3^ 3)^ (4^ 4)^ 5= 0^ 0^ 0^ 0^ 5 = 5。

通過這種方法,空間複雜度爲 O(1),而時間複雜度O(n),較爲高效。相應的代碼如下:

int find(int[] arr){
    int tmp = arr[0];
    for(int i = 1;i < arr.length; i++){
        tmp = tmp ^ arr[i];
    }
    return tmp;
}

例4.找出兩個只出現一次的數

題目:一個整型數組裏除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。例如數組爲{1,3,5,7,1,3,5,9},找出7和9。
本題可以看作上一題的升級版。思路如下:

(1)遍歷一遍數組,對所有元素使用異或操作,那麼得到的結果就是兩個出現一次的元素的異或結果。

(2)因爲這兩個元素不相等,所以異或的結果肯定不是0,也就是可以再異或的結果中找到1位不爲0的位。假設異或結果的最後一位不爲0。

(3)這樣我們就可以最後一位將原數組元素分爲兩組,一組該位全爲1,另一組該位全爲0。

(4)對於這兩組數組,最後一位爲0的一起異或,最後一位爲1的一起異或,兩組異或的結果分別對應着兩個結果。

  public static int[] findNumsAppearOnce2(int[] arr) {
      if(arr.length < 2)
          return arr;

      int[] result = new int[2];  //要返回的結果  
      int res = 0;  //第一次對所有元素進行異或操作結果  
      for(int i=0; i<arr.length; i++) {
          res ^= arr[i];
      }
      int bitIndex = 0;
      for(int i=0; i<32; i++) {  //找出異或結果爲1的位。  
          if((res>>i & 1) == 1) {
              bitIndex = i;
              break;
          }
      }
      for(int i=0; i<arr.length; i++) { //根據bitIndex爲1,將元素分爲兩組  
          if((arr[i] >> bitIndex & 1) == 1)
              result[0] ^= arr[i];   //對應位爲1,異或得到的結果  
          else
              result[1] ^= arr[i];   //對應位爲0,異或得到的結果  
      }

      return result;
  }

例5:不用四則運算求兩數字之和

原理:
通過位運算模擬兩數相加的過程

  • 兩數在二進制下相加,不考慮進位的情況下,如果某位上分別是1,0則該位結果爲1;若都是1則結果是0;都是0也是0;這情況和異或一致,這樣我們將兩數異或就得到了未進位下的兩數和
  • -而發生進位則是兩數對應位上都是1的情況,這時候我們通過按位與操作,然後進行左移就是每個位需要加上的進位
  • -將未進位下的數之和 同 發生的進位相加(注意這裏又相當於兩數相加),不斷重複直到進位爲0即可。
    int Add(int num1, int num2)
    {
    //兩個數異或:相當於每一位相加,而不考慮進位;
    //兩個數相與,並左移一位:相當於求得進位;
    //將上述兩步的結果相加,直到進位製爲0

        while( num2!=0 ){
            int sum = num1 ^ num2;           //每一位相加,而不考慮進位
            int carray = (num1 & num2) << 1; //求得進位
            num1 = sum;
            num2 = carray;
        }
        return num1;
    }

例6.找出數組中缺失的那個數

問題:給一個從0~n的連續整數數組,找出數組缺少的一個數,比如數組爲[3, 0, 1],缺少的數爲2。

這個問題我們當然可以簡單地用等差數列求和公式算出理論上數組的和,再減去數組實際的和,就能得到缺少的這個數
但如果用位運算應該怎麼做呢?

由於該數組所具有的性質,先設置一個起始量res,不斷地進行和下標值以及元素值進行異或操作:

int ret = 0; 
for (int i = 0; i < nums.length; i++) {
 ret = ret ^ i ^ nums[i];
  }

由於除了一個缺失的數字,剩下在0~n的範圍內,數組的下標是0到n-1的範圍,因此出現的下標和元素值在相同的時候都會被異或清除掉,最後剩下的就是最大數值和餘下的下標值(等於缺失的那個數值)的異或,即ret ^ nums.length。示意圖如下:
在這裏插入圖片描述
完整代碼:

int missingNumber(int[] nums) {
	    int ret = 0;
	    for (int i = 0; i < nums.length; i++) { 
	    	ret = ret ^ i ^ nums[i];
	    }
	    return ret ^ nums.length;
	}

例7.找出不大於N的最大的2的冪指數

傳統的做法就是讓 1 不斷着乘以 2。
這樣做的話,時間複雜度是 O(logn),那如果改成位運算,是否能夠提高運算效率?該怎麼做呢?

例如 N = 19,那麼轉換成二進制就是 00010011(這裏爲了方便,採用8位的二進制來表示)。那麼我們要找的數就是,把二進制中最左邊的 1 保留,後面的 1 全部變爲 0。即我們的目標數是 00010000。那麼如何獲得這個數呢?相應解法如下:

1、找到最左邊的 1,然後把它右邊的所有 0 變成 1
2、把得到的數值加 1,可以得到 00100000即 00011111 + 1 = 00100000。
3、把 得到的 00100000 向右移動一位,即可得到 00010000,即 00100000 >> 1 = 00010000。

那麼問題來了,第一步中把最左邊 1 中後面的 0 轉化爲 1 該怎麼弄呢
下面這段代碼就可以把最左邊 1 中後面的 0 全部轉化爲 1:

n |= n >> 1;
n |= n >> 2;
n |= n >> 4;

解釋
假設最左邊的 1 處於二進制位中的第 k 位(從左往右數),那麼把 n 右移一位之後,那麼得到的結果中第 k+1 位也必定爲 1,然後把 n 與右移後的結果做或運算,那麼得到的結果中第 k 和 第 k + 1 位必定是 1;
同樣的道理,再次把 n 右移兩位,那麼得到的結果中第 k+2和第 k+3 位必定是 1,然後再次做或運算,那麼就能得到第 k, k+1, k+2, k+3 都是 1,如此往復下去…

最終的代碼如下:

int findN(int n)
{
	n |= n >> 1; 
	n |= n >> 2; 
	n |= n >> 4;
	n |= n >> 8; 
	n |= n >> 16;// 整型一般是 32 位,右移十六位即可
	return (n + 1) >> 1;
}

這種做法的時間複雜度近似 O(1)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章