在上一次的博客中,我們實現了使用位操作去實現四則運算。實現整數的加減乘除。這次我們將討論位運算在算法中的一些妙用。
位運算可以進行的騷操作
在這里我將使用題目進行示例
題1:找出唯一成對的數
1-1000這1000個數放在含有1001個元素的數組中,只有唯一的一個元素值重復,其它均只出現一次。每個數組元素只能訪問一次,設計一個算法,將它找出來;不用輔助存儲空間,能否設計一 個算法實現?
這個題目有兩個要注意的點
- 數的范圍是1-1000,這個是確定的
- 不能使用輔助儲存空間
- 只有一個數字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的個數
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次)。
-
最高效的解法
這邊有個規律: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
它即是十進制的55430A | 0 = A
在這個題目(以34520為例)中我們可以先將 10000110 11011000 >> 8
右移動8位得到A = 00000000 10000110
,10000110 11011000 << 8
得到B = 11011000 00000000
,然后A | B = 11011000 10000110