Chapter1: 位运算的奇技淫巧
3. 题解:计算无符号二进制数中1的个数
题目
计算无符号整数的二进制表示中1的个数
算法
移位统计法(普通法)
这个简单算法对于每一位都需要一次操作,直到结束。所以对于32位字长,且只有最高位为1时(即最坏情况),这个算法会操作32次。
#include
#include
using namespace std;
/**Problem: 计算无符号整数v的二进制中1的个数**/
/*移位统计法*/
int main(){
unsigned int v=3;
unsigned int count=0; // 保存计算的结果
for (;v!=0; v >>= 1)//将变量v进行移位,直到最高位的1被右移舍掉后(因此该算法对有符号数无效,移位其符号位为1)
{
count += (v & 1);//如果v的最低位为1,则(v&1)会返回1,否则为0(&运算是按位与)
}
printf("%d\n",count);
return 0;
}
快速法
这种方法速度比较快,其运算次数与输入n的大小无关,只与n中1的个数有关。
如果n的二进制表示中有k个1,那么这个方法只需要循环k次即可。
其原理是不断清除n的二进制表示中最右边的1,同时累加计数器,直至n为0。
v&(v-1)
得到的是 v
去掉最低位的1
后的数
为什么v &= (v – 1)能清除最右边的1呢?因为从二进制的角度讲,v相当于在v - 1的最低位加上1
比如 7(0111)= 6(0110)+ 1(0001),所以7 & 6 = (0111)&(0110)= 6(0110),清除了7的二进制表示中最右边的1(也就是最低位的1)
又比如 8(1000)= 7(0111)+ 1(0001),所以8 & 7 = (1000)&(0111)= 0(0000),清除了8最右边的1(其实就是最高位的1,因为8的二进制中只有一个1)
int bitCount2(unsigned int v){
unsigned int count=0;
for(;v!=0;count++){
v&=(v-1);// v&(v-1)= v去掉最低位的1后的数
}
return count;
}
扩展
问题:用一条语句判断一个正整数是不是2的正整数次方
算法:
如果一个数 N
是 2
的整数次方,即 N=2^n
,则说明 N
的二进制形式为 000...1...00
的形式,即只有 1
位是 1
,其余位是 0
。N&(N-1)
返回的是去掉最低位的1的结果,如果结果为0,说明只有1个1
if(N&(N-1)==0
查表法
其实就是建立一张表,这张表存储了每一个 [0,255]
之间的数的二进制表示中 1
的个数,
对于32 bit
的无符号整数 unsigned int
, 将其切分为 4
段,每段 8 bit
,查表分别求出每段的 1
的个数再求和即可。
动态建表是在程序运行时创建表,所以速度会慢一些,静态表则会快一些,都是在用空间换时间。
对于静态表,搞一个16 bit([0,2^16])
的表,或者更极端一点 32 bit
的表,速度将会更快。
动态建表
由于表示在程序运行时动态创建的,所以速度上肯定会慢一些,把这个版本放在这里,有两个原因
介绍填表的方法,因为这个方法的确很巧妙。
类型转换,这里不能使用传统的强制转换,而是先取地址再转换成对应的指针类型。也是常用的类型转换方法。
填表原理:
对于任意一个正整数v,分奇数和偶数两种情况讨论:
-
如果v是偶数,那么v的二进制中1的个数与v/2中1的个数是相同的。
比如4和2的二进制中都有一个1,6和3的二进制中都有两个1。
为啥?因为v是由v/2左移一位而来,而移位并不会增加1的个数。
-
如果v是奇数,那么v的二进制中1的个数是v/2中1的个数+1。
比如7的二进制中有三个1,7/2 = 3的二进制中有两个1。
为啥?因为当v是奇数时,v相当于v/2左移一位再加1。
查表原理:
对于任意一个32位
无符号整数,将其分割为 4
部分,每部分 8 bit
,对于这四个部分分别求出 1
的个数,再累加起来即可。而 8bit
对应 2^8 = 256
种01组合方式,这也是为什么表的大小为256的原因。
注意类型转换的时候,先取到 v
的地址,然后转换为 unsigned char*
,这样一个 unsigned int(4 bytes=32 bit)
对应四个 unsigned char(1 bytes=8 bit)
,分别取出来计算即可。
举个例子,以87654321(十六进制)为例,先写成二进制形式, 8bit一组,共4组,这4组中1的个数分别为4,4,3,2,所以一共是13个1,如下面所示。
10000111 01100101 01000011 00100001 = 4 + 4 + 3 + 2 = 13
/*动态查表法*/
int bitCount3(unsigned int v){
//建表
unsigned char bitsSetTable[256]={0};
//初始化表
for(int i=0;i<256;i++){
bitsSetTable[i]=bitsSetTable[i/2]+(i&1);
}
unsigned int count=0;
//查表
unsigned char *p=(unsigned char*) &v;
//p[0]为 v二进制表示的低位的后8位对应的数值,...,p[3]为高位前8位对应的值
//bitsSetTable[p[0]]即为p[0]值对应的二进制的1的个数
count = bitsSetTable[p[0]]+
bitsSetTable[p[1]]+
bitsSetTable[p[2]]+
bitsSetTable[p[3]];
return count;
}
静态建表
首先构造一个包含256个元素的表 table
,table[i]
即数值 i
的二进制 1
的个数,这里的 i
是 [0,255]
之间任意一个值。
对于任意一个 32bit
无符号整数 n
,我们将其拆分成4个 8bit
,然后分别求出每个8bit
中 1
的个数,再累加求和即可.
这里用移位的方法,每次右移8位,并与 0xff(11111111)
相与,取得最低位的 8bit
(高位的与0相与所以结果为0),查表得到对应的 1
的个数,累加后继续移位,如此往复,直到v为0。
所以对于任意一个32位整数,需要查表4次。
以十进制数 2882400018
为例,其对应的二进制数为 10101011110011011110111100010010
,对应的4次查表过程如下:红色表示当前8bit
,绿色表示右移后高位补零。
第一次(v & 0xff) 101010111100110111101111 00010010
第二次((v >> 8) & 0xff) 00000000 1010101111001101 11101111
第三次((v >> 16) & 0xff)00000000 00000000 10101011 11001101
第四次((v >> 24) & 0xff)00000000 00000000 00000000 10101011
/*静态建表法*/
int bitCount4(unsigned int v){
{
unsigned int table[256] =
{
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8,
};
int count=0;
count = table[v&0xff]+table[(v>>8)&0xff]+
table[(v>>16)&0xff]+table[(v>>24)&0xff];
}
}
参考资料
[1] 算法-求二进制数中1的个数
[2] 位运算的奇技淫巧:Bit Twiddling Hacks