对一个32 位无符号整数,如何颠倒它的二进制位?
一个朴素的办法是,从低位到高位,逐位使其二进制数就位。
uint32_t reserveBits(uint32_t n) {
uint32_t ret = 0;
for (int i = 0; i < 32; i++) {
ret |= ((n >> i) & 1) << (31 - i);
}
return ret;
}
对第i
位,使用(n >> i) & 1
取得其二进制数,并且放到ret
的第31-i
位上。
不过这个算法的效率不高,对于任何数,for循环总会执行32次,我们对其改进。
uint32_t reserveBits(uint32_t n) {
uint32_t ret = 0;
for (int i = 0; i < 32 && n > 0; i++) {
ret |= (n & 1) << (31 - i);
n >>= 1;
}
return ret;
}
当n
很小的时候,只要执行几次循环n
就会变成0,提高了效率。
或许我们可以用C++
自带的bitset
,它可以像数组一样单独操作数字的每个二进制位(记得包含头文件#include <bitset>
),它的for循环总会执行16次。
uint32_t reverseBits(uint32_t n) {
bitset<32> bs(n);
char temp;
for (int i = 0; i < 16; i++) {
temp = bs[i];
bs[i]=bs[32 - i - 1];
bs[32 - i - 1] = temp;
}
return (uint32_t)bs.to_ulong();
}
如果多次调用这个函数,你该如何优化?
答案是,使用位运算分治。
uint32_t reserveBits(uint32_t n) {
n = (n & 0x55555555) << 1 | ((n >> 1) & 0x55555555);
n = (n & 0x33333333) << 2 | ((n >> 2) & 0x33333333);
n = (n & 0x0f0f0f0f) << 4 | ((n >> 4) & 0x0f0f0f0f);
n = (n & 0x00ff00ff) << 8 | ((n >> 8) & 0x00ff00ff);
return n << 16 | n >> 16;
}
上述代码共进行5步,对每位二进制用序号排列,我以表格的形式展示其翻转过程。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
– | – | – | – | – | – | – | – | – | – | – | – | – | – | – | – |
2 | 1 | 4 | 3 | 6 | 5 | 8 | 7 | 10 | 9 | 12 | 11 | 14 | 13 | 16 | 15 |
18 | 17 | 20 | 19 | 22 | 21 | 24 | 23 | 26 | 25 | 28 | 27 | 30 | 29 | 32 | 31 |
– | – | – | – | – | – | – | – | – | – | – | – | – | – | – | – |
4 | 3 | 2 | 1 | 8 | 7 | 6 | 5 | 12 | 11 | 10 | 9 | 16 | 15 | 14 | 13 |
20 | 19 | 18 | 17 | 24 | 23 | 22 | 21 | 28 | 27 | 26 | 25 | 32 | 31 | 30 | 29 |
– | – | – | – | – | – | – | – | – | – | – | – | – | – | – | – |
8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 |
24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 |
– | – | – | – | – | – | – | – | – | – | – | – | – | – | – | – |
16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 |
– | – | – | – | – | – | – | – | – | – | – | – | – | – | – | – |
32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 |
16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 |
对于位与和位或运算,我们要知道(x
是二进制位):x & 0 = 0
、x & 1 = x
,即&
是清0。x | 0 = x
、x | 1 = 1
,即|
是置1。
接下来依次解析每条语句:
n = (n & 0x55555555) << 1 | ((n >> 1) & 0x55555555);
0x55555555
变成二进制是0101 0101 0101 ...
,所以n & 0x55555555
是取其奇数位,(n & 0x55555555) << 1
是把其奇数位的二进制数放在偶数位上(原偶数位被清零)。n >> 1
是把偶数位移到奇数位上,(n >> 1) & 0x55555555
是把其偶数位的二进制数放在奇数位上(原奇数位被清零),最后再合并。于是这一条代码就是将奇偶位的二进制数交换。
n = (n & 0x33333333) << 2 | ((n >> 2) & 0x33333333);
0x33333333
变成二进制是0011 0011 0011 ...
,这一条就是隔2位交换二进制数(上一条也可看成隔1位交换二进制数)
n = (n & 0x0f0f0f0f) << 4 | ((n >> 4) & 0x0f0f0f0f);
0x0f0f0f0f
变成二进制是0000 1111 0000 1111 ...
,这一条就是隔4位交换二进制数。
n = (n & 0x00ff00ff) << 8 | ((n >> 8) & 0x00ff00ff);
0x00ff00ff
变成二进制是0000 0000 1111 1111 ...
,这一条就是隔8位交换二进制数。
return n << 16 | n >> 16;
最后一条,隔16位交换二进制数。
从自顶向上的角度看,我们把整个序列的翻转分成了:隔1位交换二进制数、隔2位交换二进制数、隔4位交换二进制数、隔8位交换二进制数、隔16位交换二进制数五个子问题,属于分治法思想。
从自底向上的角度看,这个翻转方法实际上是元素被看做完全逆序的二路归并排序,每次归并的时候都要将归并的序列交换位置。
对一个32 位无符号整数,如何计算它二进制位中“1”的个数?
一个朴素的方法是,让这个数&
自己-1的结果,这样可以每次去除它二进制位中位数最低的一个“1”。
int hammingWeight(uint32_t n) {
int ret = 0;
while(n){
n & = n - 1;
ret++;
}
return ret;
}
但看懂了之前解析的位运算分治后,我们可以使用该方法解决。
uint32_t getNum(uint32_t n) {
n = (n & 0x55555555) + ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);
n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);
n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
return n;
}
不能说有些相似,只能说是一模一样。我们倒着看,传入了一个数,对这个数进行处理,最后输出。也就是说,这个数处理之后就是其二进制位中“1”的个数,对吧?
它其实实现的思路就是把“1”尽量移到低位(对应位置都为1的时候会出现合并的情况),在进行二路归并的时候(这里把排在前面的序列称为第一路),使用新的交换方式,把第一路的序列按位+
到第二路的序列中去。所以,整个过程完全也可以看成一个元素完全逆序的二路归并排序。
最后用两个简短的序列简单展示一下该代码的思路.
序列1:0110(隔1位“交换”)->0101(隔2位“交换”)->0010
,输出“2”
序列2:1111 1111(隔1位“交换”)->1010 1010(隔2位“交换”)->0100 0100(隔4位“交换”)->0000 1000
,输出“8”