最近又看了这个问题,其实这是一个在信息论、密码学以及信息安全中非常重要的知识---汉明重量(Hamming weight).对于这个问题维基百科上有明确详细的解释和定义,并给出了高效的实现方法.记得看在编程之美中看到这个问题的时候,感觉那四种方法就很厉害了,想不到其实那都是相对简单的方法,真正高效的方法还在后面!
方法一 移位、与和相加
基本思想就是每两位相加把和存入这两位中,然后以每两位的和为单位相加把结果存入两个两位和中即四位中.为简单起见,我们举个四位二进制数的例子,设x=abcd,a,b,c,d=0|1.
要求这个四位二进制数中1的个数,我们可以把a和b相加得到和a+b,这个和a+b就是ab这两位中1的个数,原因很显然,若a=b=0,a+b=0,说明两位中没有1,;若a=0,b=1或a=1,b=0,
a+b=1,说明这两位数中有1个1,;若a=b=1,a+b=10(二进制),说明两位中有2个1;既然a+b表示ab两位中1的个数,我们可以把和a+b存入ab所占的两位.同理和c+d表示cd两位数中1的个数,我们也同样把和c+d存入cd所占的两位.思想有了,可是怎么实现呢?
在计算机中,位操作是很高效的,骨灰级的程序员往往通过位操作创造出让人拍案叫绝的程序,我们可以从位操作入手。对于x=abcd,a,b,c,d=0|1,我们要求a+b,c+d怎么求呢?我们可以把a向后移一位然后再和原数值相加,对可以通过移位操作来实现(不知道为什么下面写'+'会乱码,所以写了add,同下.而隔着那个就木事,不知道是怎么了...):
x=abcd
+ (x>>1)=0abc;
这样a和b就对齐了,c和d也对齐了。可以在x中多个a和c,(x>>1)中多了个b,我们本来是不想要这些多余位的,我们必须想办法把这些位除去,怎么除呢?想起计算机组成原理的基础知识,对某些位清零可以用与操作,嗯这里很明显是用与操作了。对于(x>>1),我们把它和0101相与&即(x>>1)&0101=0abc&0101=0a0c;对于x同样处理,x&0101=abcd&0101=0b0d.我们再把这两个结果相加:
x&0101=0a0c
+ (x>>1)&0101=0b0d;
这样以来,位对齐了多余的位也清零了再把得到的和赋值给原数值就可以了:
x=(x&0101)+((x>>1)&0101)=a+b c+d
上面
a+b和
c+d表示一个占两位的和,应该把它看成一个整体。下一步就是把
a+b和
c+d的值加起来,方法同上面的分析,不过这次应该和0011相与并且应该移两位了:
(x&0011)=00b+c
+ ((x>>2)&0011)=00a+b
x=(x&0011)+((x>>2)&0011)=a+b+c+d
这样最后的x就是abcd中1的个数。以上只是分析了4位二进制数,对于32位二进制数和64位二进制数解法原理相同.下面给出求32位无符号整数中1的个数的解法:
//常量值
const unsigned int m1=0x55555555;//0101...
const unsigned int m2=0x33333333;//00110011...
const unsigned int m4=0x0f0f0f0f;//0000111100001111...
const unsigned int m8=0x00ff00ff;
const unsigned int m16=0x0000ffff;
const unsigned int h01=0x01010101;
//代码
int popcount_1(unsigned int v){
v=(v&m1)+((v>>1)&m1);
v=(v&m2)+((v>>2)&m2);
v=(v&m4)+((v>>4)&m4);
v=(v&m8)+((v>>8)&m8);
v=(v&m16)+((v>>16)&m16);
return v;
}
解法二 上面的方法不是最好的,因为有些操作是多余的,我们可以进行代码优化,得到如下代码:
int popcount_2(unsigned int v){
v-=((v>>1)&m1);
v=(v&m2)+((v>>2)&m2);
v=(v+(v>>4))&m4;
v+=v>>8;
v+=v>>16;
return v&0x3f;
}
我们做出相关优化的解释。
1.对于v=(v&m1)+((v>>1)&m1);和v-=((v>>1)&m1);的等价性我们做一些相关解释:
v-=((v>>1)&m1);的含义是v=v-((v>>1)&m1);对此我们只要分析(v&m1)+((v>>1)&m1)=v-((v>>1)&m1);即可.
我们仍以x=abcd,a,b,c,d=0|1,来分析,
对于等式(v&m1)+((v>>1)&m1)=v-((v>>1)&m1)有
0b0d+0a0c=abcd-0a-c
=> 0b0d+2*0a0c=abcd
=> 0b0d+a0c0=abcd
=> abcd=abcd
很明显是成立的,其实有下面的推理,
(v&m1)+((v>>1)&m1)=v-((v>>1)&m1);
=>(v&m1)+2*((v>>1)&m1)=v
=>(v&m1)+2*((v>>1)&m1)=v
=>(v&m1)+((v>>1)&m1)<<'2'=v
=>v=v
这样以来就由原来的1个移位操作、两个&操作和1个+操作变为1个移位操作、1个&操作和1个-操作,减少了1个&操作。
2.对于下面的优化,我们做出相应的解释:
v=(v+(v>>4))&m4;
设x=abcd efgh,每一位都为1或0,原本的算式是:
v=(v&m4)+((v>>4)&m4);
0000 efgh
+ 0000 abcd
变为:
abcd efgh
+ 0000 abcd
对于一个8位二进制数最多有8个1,8用4位二进制数完全足以表示,即efgh+abcd的和一定可以放在4位的二进制数内,压根就不需要向前进位,故可以先把abcd efgh中的前4位留着,不需要事先进行&操作变为0000 efgh.这样和值放在低4位中,再把abcd清零,于是就有了优化后的等式:
v=(v+(v>>4))&m4;
对于下面的优化:
v+=v>>8;
v+=v>>16;
可以做相同的理解,32位数最多有32个1,最多只需用6位表示,最后的和把前面的位清零即可得到最后的值:
所以要return v&0x3f;
解法三 其实还可以优化,这里只可以对后两步进行优化了,代码如下:
int popcount_3(unsigned int v){
v-=((v>>1)&m1);
v=(v&m2)+((v>>2)&m2);
v=(v+(v>>4))&m4;
return (v*h01)>>24;
}
读者可以试着解释优化的原理.如有不对之处请多多指正,不胜感激!