位運算的操作與演算法

段小輝發表於2019-07-01

在上一次的部落格中,我們實現了使用位操作去實現四則運算。實現整數的加減乘除。這次我們將討論位運算在演算法中的一些妙用。

位運算可以進行的騷操作

在這裡我將使用題目進行示例

題1:找出唯一成對的數

1-1000這1000個數放在含有1001個元素的陣列中,只有唯一的一個元素值重複,其它均只出現一次。每個陣列元素只能訪問一次,設計一個演算法,將它找出來;不用輔助儲存空間,能否設計一 個演算法實現?

這個題目有兩個要注意的點

  1. 數的範圍是1-1000,這個是確定的
  2. 不能使用輔助儲存空間
  3. 只有一個數字g重複

那麼我們應該怎麼去解決這個題目呢?在這裡我們既然講了位運算,那麼肯定是使用|&^等來解決這些問題。

首先我們得知道:

A ^ A = 0 , A ^ 0 = A

那麼我們可以想想,假如我們將題目中的陣列與 1~1000進行異或操作那麼剩下的值就是那一個重複的值。

​ 簡單的來個示例,假如陣列是[1,2,3,4,3]

1 ^ 2 ^ 3 ^ 4 ^ 3 ^ 1 ^ 2 ^ 3 ^ 4 = 1 ^ 1 ^ 2 ^ 2 ^ 3 ^ 3 ^ 4 ^ 4 ^ 3 = 0 ^ 3 = 3

import java.util.Arrays;
import java.util.Random;
public class SameWord {
    public static void main(String[] args) {
        // 不重複的數字有1000個
        int N = 1000;
        // 陣列的容量為10,其中有一個為重複的
        int[] arry = new int[N + 1];

        for (int i = 0; i < N; i++) {
            arry[i] = i + 1;
        }
        Random random = new Random();
        // 產生1~N的隨機數
        int same = random.nextInt(N)+1;
        int position = random.nextInt(N);
        // 將重複的值隨機調換位置
        arry[N] = arry[position];
        arry[position] = same;
        // 前面一部分就是為了產生1001大小的陣列,其中有一個是重複的
                
        // 進行異或操作 【1^2^3^4……】
        int x = 0;
        for (int i = 0; i < N; i++) {
            x = (x ^ (i+1));
        }
        
        // 對陣列進行異或操作
        int y = 0;
        for (int i = 0; i < N + 1; i++) {
            y = (arry[i] ^ y);
        }
        // 列印重複的值
        System.out.println(x^y);
    }
}

題2:找出單個值

一個陣列裡除了某一個數字之外,其他的數字都出現了兩次。請寫程式找出這個只出現一次的數字。

emm,假如弄懂了上面一個題目,這個題目就輕而易舉了

public void getSingle(){
    int[] a = {1,2,3,2,1,3,4};

    int single = 0;
    for (int i : a) {
        single = single^i;
    }
    System.out.println(single);
}

題三:找出1的個數

請實現一個函式,輸入一個整數,輸出該數二進位制表示中1的個數

例:9的二進位制表示為1001,有2位是1

這個題目挺簡單的。有2個方向可以去解決

  1. 通過移位獲得1的個數

1001 & 1 = 1 , 1001 >> 1 = 100,100 & 1 = 0

public void getNum(){
    int n = 255;
    int count = 0;
    while(n!=0){
        if((n & 1) == 1){
            count ++;
        }
        n = n>>1;
    }
    System.out.println("個數是:"+count);
}

​ 這種解法其實有一定問題的,因為如果去移動負數的話就會涼涼,陷入死迴圈(負數右移,最左邊的那個1會一直存在)。那麼我們怎麼解決這個方法呢?既然我們不能移動n,那麼我們可以移動相與的那個數啊

1001 & 1 = 1, 1<<1 = 10,1001&10 = 0

public void getNum2(){
    int n = 222;
    int flag = 1;
    int count = 0;
    
    while(flag >=1){
        // 這個地方不是n&flag == 1了
        if((n&flag) > 0){
            count ++;
        }
        flag = flag << 1;
    }
    System.out.println("個數是:"+count);
}

我們可以去考慮下這個的時間複雜度。實際上,無論你要求解的數值有多小,它都要迴圈32次(因為int為4個位元組,需要迴圈32次)。

  1. 最高效的解法

    這邊有個規律:n&(n-1)能夠將n的最右邊的1去掉。

    那麼根據這個規律,如果我們將右邊的1去掉,去掉的次數也就是二進位制中1的個數

    public void getNum3(){
        int n = 233;
        int count = 0;
        while(n>0){
            count ++;
            n = (n -1)&n;
        }
        System.out.println("個數是:"+count);
    }

題四:保證不溢位地取整數平均值

求平均值我們一般是使用相加來進行操作的,但是如果值比較大呢,造成溢位怎麼辦?實際上我們知道溢位就是因為進位造成的,那麼我們就可以使用位來解決這個方法。

10 二進位制 1010
14 二進位制 1110
公共部分: 1010
不同部分的和: 0100
不同部分除以2:0010
平均數 = 1010(相同部分) + 0010(不同部分的平均數) = 1100
因此二者平均數為12

以上的操作我們可以用位運算來替代:

公共部分 = a & b
不同部分的平均值 = (a ^ b) >> 1
平均值 = 公共部分 + 不同部分的平均值 = (a & b) + ((a ^ b) >> 1)

public void aver(){
    int a = 10;
    int b = 220;
    int averNum = (a&b) + ((a^b)>>1);
    System.out.println("平均值是:"+averNum);
}

題五:高低位交換

給出一個16位的無符號整數。稱這個二進位制數的前8位為“高位”,後8位為“低位”。現在寫一程式將它的高低位交換。例如,數34520用二進位制表示為:
10000110 11011000
將它的高低位進行交換,我們得到了一個新的二進位制數:
11011000 10000110
它即是十進位制的55430

A | 0 = A

在這個題目(以34520為例)中我們可以先將 10000110 11011000 >> 8右移動8位得到A = 00000000 1000011010000110 11011000 << 8得到B = 11011000 00000000,然後A | B = 11011000 10000110

相關文章