昨天又看到了一种bitcount(数一个整数中1的个数)的算法,想自己总结一下各种不同的算法。上网找了一下,发现zdd已经总结过了,这里想加入点自己的理解。
对于没有具体说明的算法,可以参考zdd的算法-求二进制数中1的个数。
1. 普通法:就是简单地循环迭代。(Iterated count)
2. 查表法:以空间换取时间效率。可以是4bit-表,8-bit表或者16-bit表,表越大越快,但是所占的内存空间也就更大。对于32位的整数,也可以使用11-bit的表,把整数分成10-bit, 11-bit, 11-bit三段。
3. 快速法:运算次数与输入n的大小无关,只与n中1的个数有关。
1 /***********parse***********/ 2 3 int bitcount (unsigned int n) { 4 int count = 0 ; 5 while (n) { 6 count++ ; 7 n &= (n - 1) ; 8 } 9 return count ; 10 }
其中的关键就是n &= (n-1), 它把n整数的最右边的1变成了0。所以循环次数只是n中1的个数。上述适用于整数n是parse的。如果整数n是dense的,也就是说有比较多的1,那么可以先把n取反再算。
1 /***********dense***********/ 2 3 int bitcount (unsigned int n) { 4 int count = 8 * sizeof(int) ; 5 n ^= (unsigned int) - 1 ; 6 while (n) { 7 count-- ; 8 n &= (n - 1) ; 9 } 10 return count ; 11 }
4. 平行法:其实我更愿意把这种方法叫做“二分法”,基本的思想类似“二分法”。
1 #define TO(c) (0x1u << (c)) 2 #define MASK(c) \ 3 (((unsigned int)(-1)) / (TO(TO(c)) + 1u)) 4 #define COUNT(x,c) \ 5 ((x) & MASK(c)) + (((x) >> (TO(c))) & MASK(c)) 6 7 int bitcount (unsigned int n) { 8 n = COUNT(n, 0) ; 9 n = COUNT(n, 1) ; 10 n = COUNT(n, 2) ; 11 n = COUNT(n, 3) ; 12 n = COUNT(n, 4) ; 13 return n ; 14 }
开始n的二进制看成以1-bit为1组,那么0表示该组1的个数为0,1表示该组1的个数为1。然后把相邻组的1的个数相加,变成2-bit为一组。每2-bit存储的是2-bit为一组的1的个数。接着再以4-bit为一组,依次类推。
如果以二分法来看,很容易理解。假设整数n为32位,那么1的个数是高16位为一组1的个数加上低16位为一组1的个数,然后把16-bit再二分,依次类推。
5. MIT HAKMEM: 很好很强大,太geek了。
1 int BitCount5(unsigned int n) 2 { 3 unsigned int tmp = n - ((n >> 1) & 033333333333) - ((n >> 2) & 011111111111); 4 return ((tmp + (tmp >> 3)) & 030707070707) % 63; 5 }
先看第4行
return ((tmp + (tmp >> 3)) & 030707070707) % 63;
说明是以3-bit为一组相加,得到的是6-bit为一组,结果用63取模。
为什么以63取模,其实是有根据的。
对于多项式f(n) = A0 * n0 + A1 * n1 + A2 * n2 + .... = Σ(Ai) (mod n-1)。
于是如果Ai代表了各组的1的位数,那么用n-1取模可以直接算出Σ(Ai)。不过要求Σ(Ai) < n-1, n如果是32位的,那么32 < n-1, 于是取n = 64。
即6-bit为一组。然后我们可以用上面的二分法,6-bit为一组可以由3-bit为一组合并得到,于是有了
(tmp + (tmp >> 3) & 030707070707)。
(ps: 这里还有一个小的trick, 如果按照上面的二分方式,应该是tmp & 030707070707 + (tmp>>3) & 030707070707, 由于6-bit为一组最大1的个数是6,即(110)2,不可能发生溢出,用三位足够保存,所以可以把&操作提取出来)。
上述分析都是基于tmp中是以3-bit为一组,每组中的值是整数n相应3位中1的个数。那么如何得出这个数值呢?很自然地想到就是上述二分法中的办法
tmp = n & 0x01010101 + (n>>1) & 0x01010101 + (n>>2) & 0x01010101;
不过HAKMEM中少了一次&操作,很厉害。
对于3位(abc)2 = 4a+2b+c,
(abc)2>>1 = (0ab)2 =2a+b,
(abc)2>>2 = (00a)2 =a
于是第3行就变成a+b+c。
大家可以仿照该方法考虑下64bit整数如何做类似的处理。对于并行法,利用上面得出的结论n>33, 还可以化简为如下(不过速度不一定更快,取决于mod操作)
1 #define TO(c) (0x1u << (c)) 2 #define MASK(c) \ 3 (((unsigned int)(-1)) / (TO(TO(c)) + 1u)) 4 #define COUNT(x,c) \ 5 ((x) & MASK(c)) + (((x) >> (TO(c))) & MASK(c)) 6 7 int bitcount (unsigned int n) { 8 n = COUNT(n, 0) ; 9 n = COUNT(n, 1) ; 10 n = COUNT(n, 2) ;13 return n%255 ; 14 }
Reference: