位移的妙用
位1的个数
有两种思路,让1不断左移或者将原始数据不断右移。例如将原始数据右移就是:
00000100100100010000110001000100
& 00000000000000000000000000000001
= 00000000000000000000000000000000
很明显此时就可以判断出第二位是0,然后继续将原始数据右移就可以依次判断出每个位置是不是1了。因此是不是1,计算一下(n>>i) & 1就可以了,所以代码顺理成章:
public int hammingWeight(int n) {
int count = 0;
for (int i = 0; i < 32; i++) {
count += (n >> i) & 1;
}
return count;
}
按位与运算有一个性质:对于整数 n,计算n & (n−1) 的结果为将 n 的二进制表示的最后一个 1 变成 0。利用这条性质,令 n=n & (n−1),则 n 的二进制表示中的 1 的数量减少一个。重复该操作,直到 n 的二进制表示中的全部数位都变成 0,则操作次数即为 n 的位 1 的个数。
由此可以将上面的代码优化:
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
n = n & (n - 1);
count++;
}
return count;
}
比特位计数
最直观的方法是对从 0 到 num 的每个数直接计算"一比特数"。每个int 型的数都可以用 32 位二进制数表示,只要遍历其二进制表示的每一位即可得到1 的数目。
public int[] countBits(int n) {
int[] bits = new int[n + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j < 32; j++) {
bits[i] += (i >> j) & 1;
}
}
return bits;
}
利用位运算的技巧,可以提升计算速度。按位与运算(&)的一个性质是:对于任意整数 x,令 x=x&(x−1),该运算将 x 的二进制表示的最后一个 1 变成 0。因此,对 x 重复该操作,直到 x 变成0,则操作次数即为 x 的「一比特数」。
public int[] countBits(int num) {
int[] bits = new int[num + 1];
for (int i = 0; i <= num; i++) {
bits[i] = countOnes(i);
}
return bits;
}
public int countOnes(int x) {
int ones = 0;
while (x > 0) {
x &= (x - 1);
ones++;
}
return ones;
}
颠倒无符号整数
我们注意到对于 n 的二进制表示的从低到高第 i 位,在颠倒之后变成第 31-i 位( 0≤i<32),所以可以从低到高遍历 n 的二进制表示的每一位,将其放到其在颠倒之后的位置,最后相加即可。
理解之后,实现就比较容易了。由于 Java不存在无符号类型,所有的表示整数的类型都是有符号类型,因此需要区分算术右移和逻辑右移,在Java 中,算术右移的符号是 >>,逻辑右移的符号是 >>>。
public int reverseBits(int n) {
int reversed = 0, power = 31;
while (n != 0) {
reversed += (n & 1) << power;
n >>>= 1;
power--;
}
return reversed;
}
位实现加减乘除专题
位运算实现加法
两个位加的时候,我们无非就考虑两个问题:进位部分是什么,不进位部分是什么。从上面的结果可以看到,对于a和b两个数不进位部分的情况是:相同为0,不同为1,这不就是a⊕b吗?
而对于进位,我们发现只有a和b都是1的时候才会进位,而且进位只能是1,这不就是a&b=1吗?然后位数由1位变成了两位,也就是上面的[4]的样子,那怎么将1向前挪一下呢?手动移位一下就好了,也就是(a & b) << 1。所以我们得到两条结论:
- 不进位部分:用a⊕b计算就可以了。
- 是否进位,以及进位值使用(a & b) << 1计算就可以了。
于是,我们可以将整数 a 和 b 的和,拆分为 a 和 b 的无进位加法结果与进位结果的和,代码就是:
public int getSum(int a, int b) {
while (b != 0) {
int sign = (a & b) << 1;
a = a ^ b;
b = sign;
}
return a;
}
递归乘法
如果不让用*来计算,一种是将一个作为循环的参数,对另一个进行累加,但是这样效率太低,所以我们还是要考虑位运算。
首先,求得A和B的最小值和最大值,对其中的最小值当做乘数(为什么选最小值,因为选最小值当乘数,可以算的少),将其拆分成2的幂的和,即min = a_0 * 2^0 + a_1 * 2^1 + ... + a_i * 2^i + ...其中a_i取0或者1。其实就是用二进制的视角去看待min,比如12用二进制表示就是1100,即1000+0100。例如:
13 * 12 = 13 * (8 + 4) = 13 * 8 + 13 * 4 = (13 << 3) + (13 << 2);
上面仍然需要左移5次,存在重复计算,可以进一步简化:
假设我们需要的结果是ans,
定义临时变量:tmp=13<<2 =52计算之后,可以先让ans=52
然后tmp继续左移一次tmp=52<<1=104,此时再让ans=ans+tmp
这样只要执行三次移位和一次加法,实现代码:
public int multiply(int A, int B) {
int min = Math.min(A, B);
int max = Math.max(A, B);
int ans = 0;
for (int i = 0; min != 0; i++) {
//位为1时才更新ans,否则max一直更新
if ((min & 1) == 1) {
ans += max;
}
min >>= 1;
max+=max;
}
return ans;
}