计算整数的二进制中1的个数
参考文章:popcount 算法分析 - 知乎 (zhihu.com)
刷完leetcode的1342题后,对算法和二进制计算的精妙之处感到佩服,由于算法的底子并不好,搞了一晚上才弄懂一点,参考文章讲的十分详细,这里针对我觉得比较难理解的并行计算parallel_popcnt和nifty_pop分析。有错欢迎指出。
parallel_popcnt
并行计算采用了分治的思想
当我们每一次在做popcount计算时,都是由k位的结果计算2k位。
一开始时k = 1,即每一位分别位一组,记录着该位1的个数。
而 n = a * 2k + b a为高k位,b位低k位。
则popcount(n) = a+b。
例如 k = 1时,n = 1 * 21 + 1 = 3
则popcount(3) = 2
那么我们现在的问题就是,怎么样将n进行分组?从而分而治之。
对n进行&运算可以达到分组的效果。
第一次的计算为两两一组,所以可以让n对01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01进行&运算
还是以0xFFFFFFFF举例,对于每两位 11(二进制) 来说,就是11 & 01的操作
那么popcount(11(二进制)) = 11 & 01 + (11 >> 1) & 01;
对于4位一组,可以用0011 = 0x3,对于8位一组,可以用00001111=0x0F,对于16位一组,可以用00000000 11111111=0x00FF,最后对32位一组,可以用00000000 00000000 11111111 11111111 = 0x0000FFFF进行与操作,最终对32位的整数计算完毕。
结合代码
int parallel_popcnt(uint32_t n) { #define POW2(c) (1U << (c)) #define MASK(c) (static_cast<uint32_t>(-1) / (POW2(POW2(c)) + 1U)) #define COUNT(x, c) ((x) & MASK(c)) + (((x)>>(POW2(c))) & MASK(c)) n = COUNT(n, 0); n = COUNT(n, 1); n = COUNT(n, 2); n = COUNT(n, 3); n = COUNT(n, 4); // n = COUNT(n, 5); // uncomment this line for 64-bit integers return n; #undef COUNT #undef MASK #undef POW2 }
static_cast<uint32_t>(-1) = 0xFFFFFFFF(32位二进制的这个数表示十进制的-1)
MASK© -> FFFFFFFF / (2 ^ (2^c) + 1)
- 01010101 01010101 01010101 01010101 = 55555555 = FFFFFFFF / (0x2+1)
- 00110011 00110011 00110011 00110011 = 33333333 = FFFFFFFF / (0x4+1)
- 00001111 00001111 00001111 00001111 = 0F0F0F0F = FFFFFFFF / (0x10+1)
- 00000000 11111111 00000000 11111111 = 00FF00FF = FFFFFFFF / (0x100+1)
- 00000000 00000000 11111111 11111111 = 0000FFFF = FFFFFFFF / (0x10000+1)
根据图中对于32位整数的推算,需要对n进行五次迭代就可以得到n二进制中1的个数
MASK中(x)>>(POW2©)表示x右移2c位,即对应x的高2c位
nifty_popcnt
直接看代码
int nifty_popcnt(uint32_t n)
{
constexpr uint32_t max = std::numeric_limits<uint32_t>::max();
constexpr uint32_t MASK_01010101 = max / 3;
constexpr uint32_t MASK_00110011 = max / 5;
constexpr uint32_t MASK_00001111 = max /17;
n = (n & MASK_01010101) + ((n>>1) & MASK_01010101);
n = (n & MASK_00110011) + ((n>>2) & MASK_00110011);
n = (n & MASK_00001111) + ((n>>4) & MASK_00001111);
return n % 255 ;
}
这里直接引用参考文章的一段解释(popcount 算法分析 - 知乎 (zhihu.com)):
一个K进制数B ( BnBn−1⋯B1B0 ),n表示共有n位数,且 0≤Bi<K ,数 B 可以记作B=Bn∗Kn−1+Bn−1∗Kn−2+⋯+B1∗K+B0 。有等式 Ki≡1mod(K−1) ,可以用 Ki=((K−1)+1)i 二项式展开,或用数学归纳法证明此结论。代入上式,有 B≡Bn+Bn−1+…+B1+B0mod(K−1) 。于是有结论:**一个K进制数模(K-1)的结果,等于此K进制数的各位相加再模K-1。**这个结论不难理解,就是小学数学里被告知如何判断一个十进制数能否被9整除的方法——每个位上的数加起来能否被9整除。如果位数太多,加起来后的数依然很大,你可以多次套用这一法则。
于是,如果能确定 ∑i=0nBi=Bn+Bn−1+…+B1+B0<K−1 的话,就可以加强结论:popcount(B) = B % (K-1)。%是取模操作。上面每8位一组,相当于 28=256 进制,所以用了255这个数;为了使用上面的等式计算,必须至少得3次迭代。2次迭代创造 222=16 进制,而对于一个32位整形,popcount 的最大值为32, 222−1<32<223−1 ,所以需要3次迭代。想想一下64位整形 uint64_t,popcount 可能的最大取值是64,这里要选取的数是511。
nifty_popcnt其实和parallel_pop的做法几乎一样,只是在迭代三次后,直接返回了n%255;
详细的数学解释可以看参考文档,这里对于特例进行解释。
当初始n = 0xFFFFFFFF时,n二进制1的个数为32,即32位整数二进制1最多有32个。
迭代三次后,n就变为了四组,每八位一组。每组的值为0001000。
即每一组的最大值不超过8
所以K-1 > 32即可
,n二进制1的个数为32,即32位整数二进制1最多有32个。
迭代三次后,n就变为了四组,每八位一组。每组的值为0001000。
即每一组的最大值不超过8
所以K-1 > 32即可
由于两次迭代后只有十六进制 16 -1 < 32,故要进行第三次迭代。