计算一个字中1位的数目有时被称为“种群计数”
以一个32位的int为例
最朴素的方法:检查最后一位,计数,然后无符号右移。
这样的算法复杂度为O(logi),最坏的时候要做32次循环。
O(m)算法,m为二进制中1的个数
x-1操作将x的二进制表示中的最右边的1改成0,而该位右边所有的0都改成1,x&(x-1)就将x二进制中最右边的1去掉,于是
复杂度为O(m),在种群稀疏的字计数中效率尤其明显。如果较为密集,可以改为计数0的个数,然后从32中减去
计数0的个数与计数1的个数操作是对称的:x+1将最右边的0改成1,该0右边的所有1都改成0,x|(x+1)就将x二进制中最右边的0去掉,循环检测条件改成检测该数是否为-1即可。
分治算法
上述的O(m)算法已经足够巧妙了,然而还存在一般情况下更好的算法。
首先计算二进制位中相邻两个位的和,并将结果存放在这两位中。然后计算相邻的两个两位的和,放在这四位中,以此类推。。。例如
1 0 1 1 1 1 0 0 0 1 1 0 0 0 1 1 0 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1
0 1|1 0|1 0|0 0|0 1|0 1|0 0|1 0|0 1|1 0|1 0|0 1|1 0|1 0|1 0|1 0
0 0 1 1|0 0 1 0| 0 0 1 0|0 0 1 0|0 0 1 1|0 0 1 1 |0 1 0 0|0 1 0 0
0 0 0 0 0 1 0 1| 0 0 0 0 0 1 0 0|0 0 0 0 0 1 1 0|0 0 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 | 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 1 1
最后的结果就是1个个数,上例中为23
每一步都可以用一个mask与移位之后加法来实现
以下是VC6.0下反编译的结果
每一步需要7个汇编指令,一共需要35个汇编指令。
以下是bit_count2的反编译结果
每一个循环需要10个汇编指令,可见在位数超过4的时候,效率就已经不如bit_count3。
优化是没有止境的
细细分析bit_count3,还有很多值得优化的地方。
首先对于一个二进制数各位数字的和有公式
pop(x)=x-(x>>1)-(x>>2)-...-(x>>31)
证明:对于一般的32位字,unsigned int的二进制表示的第i位可以表示为
bi=(x>>i)-((x>>i+1)<<1)
从0到31累加即可得到结论。
二进制中1的个数也就是二进制中的各位数字之和,一般情况下用上述公式进行计算的效率是不如bit_count3的,但是,在二进制数只有2位的时候,可以直接用x-(x>>1)来计算,可以利用这个结论来优化bit_count3,bit_count3的第一步就是计算相邻两位的各位数字之和。
i=(i&0x55555555)+((i>>1)&0x55555555);
就可以改写为
i=i-((i>>1)&0x55555555);
另外,对于不可能对相邻位产生进位的加法,不需要进行与运算。
2个2位的时候最多为10+10,有进位,故第二步不能省
2个4位的时候最多为100+100,无进位,第三步可以省,但是由于第四步要进行8位相加,故仍然需要用0x0f0f0f0f进行掩码
2个8位的时候最多为1000+1000,可以省,而且剩余位不会对结果产生影响,不需要掩码
16位的时候同理
位数不会超过32,所以最后结果需要对x进行0x0000003F的掩码
这样就产生了bit_count4
反汇编的结果如下
需要31条汇编指令。
如果我们对每4位进行各位之和的计算
即前两步为
i-=((i>>1)&0x77777777)+((i>>2)&0x33333333)+((i>>3)&0x11111111);
没什么效果。。。若直接用汇编编写,效率应该还有待提高。。。
真的没有止境
如果对空间的要求不是那么苛刻的话,为了将时间效率发挥到极致,可以采用查表法。
例如8位的查表法,代码如下
效率更高请采用16位查表法。。。。需要65536个字节的空间来存储。。。。
位图中的种群计数
在一个数组的种群计数中,我们可以用上述方法依次求出每个元素的计数,然后累加。
但是,注意到,4位的计数最多可以到15,而按照上述算法每一个数的4位最多只有4,那么我们运用上述分治算法的前两步,然后3个元素相加,求得和之后,再对和进行后面步数的处理,而8位的情况,每个8位最多是24,8位可以统计255/24=10个字,以此类推,这样的算法效率会很高,但是过多的循环控制语句会大大降低算法节省的效率。
由此,只利用一个中间层次,计算出4个8位部分的和之后,每个8位部分的和最多为8,最多可以将255/8=31个8位和相加而不溢出。以下代码来自Hacker's Delight。。。