__builtin_popcount原理

小学期题用了这个函数觉得十分高级学习一个

这个函数用来数二进制数中1的个数正常的操作是unsigned int二进制表示下最右端是不是1若是1则计数器+1再右移重复32次这样的操作需要进行32次int运算效率不够高这个问题可以二分解决

首先看一个结论对等长含前导0的二进制数$a$$b$设他们长为$k$并记将$a$接在$b$前面形成的$2k$位的二进制数为$(a|b)$那么$a+b=(a|b)>>k+(a|b)$这是显而易见的

这个操作将直接的加法转换为了位运算当k较小时可以将多个k拼在一起放在int32中同时进行多个位运算这样就有了提高加法效率的可能相当于多线程了然后若k=1时将和用2位来存储再取k=2再将和用4位存储再取k=4…然后就可以进行二分$O(\log n)$时间内求n个和这是计算机的特性造就的性能提升

下面模拟一下这个过程现在有一个int8数n=01011100b首先对每一位这一位本身的数字恰好记录了这一位上1的个数这一位是1就有1个1是0就有0个1接下来要将这些1加起来首先取k=1将n分为8位然后相邻两位相加此时每两位指1-2,3-4这样的两位共同构成的二进制数表示这两位上1的和例如(0|1|0|1|1|1|0|0)相邻两位相加后应为(01|01|10|00)这个操作可以用刚刚的公式来进行具体来讲两位一格分为4格每格里都是右移一位后相加由于位运算可以同时进行那么16格同时运算就应该是(n-偶数位-置0 + (n-奇数位-置0 >> 1))=((01|01|01|00)+(00|00|10|00)>>1)=(01|01|01|00)+(_0|00|01|00)=(01|01|10|00)偶从右数起此时每格里都完成了一次上述运算接下来两位一份分为四份相邻两份再次相加即(01+01|10+00)=(0010|0010)为了求和要构造$2k$位的格子于是四位一格分成2格为(0101|1000)求和过程应为(00|01|00|00)+(01|00|10|00)>>2=(00|01|00|00)+(00|01|00|10)=(0010|0010)最后是0010+0010=(0000|0010)+(0010|0000)>>4=(0000|0010)+(0000|0010)=(0000|0100)=4为n中1的个数过程不再赘述

语言表达能力差也不知道说清楚没

最后给出代码

1
2
3
4
5
6
7
8
9
unsigned int popcount(unsigned int n)
{
n = (n & 0x55555555) + ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);
n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);
n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
return n;
}

gcc内置了这个函数名字是__builtin_popcount当正常函数调用就行即__builtin_popcount(n);