二进制中1的个数

计算整数的二进制中1的个数

参考文章:popcount 算法分析 - 知乎 (zhihu.com)

刷完leetcode的1342题后,对算法和二进制计算的精妙之处感到佩服,由于算法的底子并不好,搞了一晚上才弄懂一点,参考文章讲的十分详细,这里针对我觉得比较难理解的并行计算parallel_popcnt和nifty_pop分析。有错欢迎指出。

parallel_popcnt

并行计算采用了分治的思想

popcount

当我们每一次在做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)

  1. 01010101 01010101 01010101 01010101 = 55555555 = FFFFFFFF / (0x2+1)
  2. 00110011 00110011 00110011 00110011 = 33333333 = FFFFFFFF / (0x4+1)
  3. 00001111 00001111 00001111 00001111 = 0F0F0F0F = FFFFFFFF / (0x10+1)
  4. 00000000 11111111 00000000 11111111 = 00FF00FF = FFFFFFFF / (0x100+1)
  5. 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,故要进行第三次迭代。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值