(转)位计算的诸多算法(计算二进制1的个数)
位计算(Bit Count)
最近在重写黑白棋的底层数据结构,用位棋盘作为棋盘格式。用位棋盘计算移动力是相当快的,每次在一个方向上产生所有合法的着法,经8次(8个方向)就可以得到所有合法的着法,并将long数据中相应位置1,最后计算的个数。计算一个整数中多少位被置为1,是一个比较常见的问题,http://infolab.stanford.edu/~manku/bitcount/bitcount.html 介绍了很多计算的方法。
1.循环
Iterated Count
public int bitCount_Iterated(long n){
int count = 0;
while(n != 0){
count += (int)(n & 0x1L);
n >>= 1;
}
return count ;
}
迭代方式求解是最直观、简单的,但是效率不高。
Sparse Count
public int bitCount_Sparse(long n){
int count = 0;
while(n != 0){
count ++;
n &= (n - 1);
}
return count ;
}
算法的循环次数与1的个数成正比,因此在1的个数比较少时,效率比较高。主要思想是每次减少一个1,n &= (n - 1)将最低位的1置为0。
Dense Count
public int bitCount_Dense(long n){
int count = 64;
n = ~n;
while(n != 0){
count --;
n &= (n - 1);
}
return count ;
}
算法的循环次数与0的个数成正比,因此在1的个数比较多时,效率比较高。与Sparse Count一样的思想,只是将0和1翻转,计算0的个数,总的位数减去0的个数就是1的个数。
2.查表
private int[] BIT_COUNT_TABLE_8;
private int[] BIT_COUNT_TABLE_16;
public void GenerateBitCountTable(boolean bits8){
int size, length, bits, count;
int[] table;
if(bits8){
BIT_COUNT_TABLE_8 = new int[1 << 8];
table = BIT_COUNT_TABLE_8;
size = 1 << 8;
length = 8;
}else{
BIT_COUNT_TABLE_16 = new int[1 << 16];
table = BIT_COUNT_TABLE_16;
size = 1 << 16;
length = 16;
}
for(int i = 0; i < size; i++){
bits = i;
count = 0;
for(int j = 0; j < length; j++){
if(((bits >>> j) & 1) != 0)
count++;
}
table[i] = count;
}
}
Precompute 8bits Count
public int bitCount_Precomput8(long n){
int[] table = BIT_COUNT_TABLE_8;
return table[(int)(n & 0xffL)]
+ table[(int)((n >> 8) & 0xffL)]
+ table[(int)((n >>> 16) & 0xffL)]
+ table[(int)((n >>> 24) & 0xffL)]
+ table[(int)((n >>> 32) & 0xffL)]
+ table[(int)((n >>> 40) & 0xffL)]
+ table[(int)((n >>> 48) & 0xffL)]
+ table[(int)((n >>> 56) & 0xffL)];
}
每8位查表。
Precompute 16bits Count
public int bitCount_Precomput16(long n){
int[] table = BIT_COUNT_TABLE_16;
return table[(int)(n & 0xffffL)]
+ table[(int)((n >>> 16) & 0xffffL)]
+ table[(int)((n >>> 32) & 0xffffL)]
+ table[(int)((n >>> 48) & 0xffffL)];
}
每16位查表
3. 平行算法
private final long MASK_1 = 0x5555555555555555L;
private final long MASK_2 = 0x3333333333333333L;
private final long MASK_4 = 0x0F0F0F0F0F0F0F0FL;
private final long MASK_8 = 0x00FF00FF00FF00FFL;
private final long MASK_16 = 0x0000FFFF0000FFFFL;
private final long MASK_32 = 0x00000000FFFFFFFFL;
Parallel Count
public int bitCount_Parallel(long n){
n = (n & MASK_1) + ((n >>> 1) & MASK_1);
n = (n & MASK_2) + ((n >>> 2) & MASK_2);
n = (n & MASK_4) + ((n >>> 4) & MASK_4);
n = (n & MASK_8) + ((n >>> 8) & MASK_8);
n = (n & MASK_16) + ((n >>> 16) & MASK_16);
n = (n & MASK_32) + ((n >>> 32) & MASK_32);
return (int)n;
}
平行算法有点象两递归的过程,要求64位中1的个数,就先分别求高32位和低32位中1的个数,要求32位中1个的个数,就先求这32位中高16位和低16位中1的个数.......当然这只是便于理解,实际求解肯定不会这样,而是采用自底向上的方式,先将相邻2位的个数求出来n = (n & MASK_1) + ((n >>> 1) & MASK_1),然后再将相邻4位的求出来n = (n & MASK_2) + ((n >>> 2) & MASK_2).......直到将相邻32位中的1的个数相加n = (n & MASK_32) + ((n >>> 32) & MASK_32)。
Nifty Count
public int bitCount_Nifty(long n){
n = (n & MASK_1) + ((n >>> 1) & MASK_1) ;
n = (n & MASK_2) + ((n >>> 2) & MASK_2) ;
n = (n & MASK_4) + ((n >>> 4) & MASK_4) ;
return (int)(n % 255L);
}
前3次和Parallel Count一样,这时每8位中的1的个数已经求得。这样将每个字节看作一个数bi,其中i = 0, 1,2....7,且bi <= 8。b7 b6 b5 b4 b3 b2 b1 b0然后把它写成b7 * (256 ^ 7) + b6 * (256 ^ 6) + b5 * (256 ^ 5) + b4 * (256 ^ 4) + b3 * (256 ^ 3) + b2 * (256 ^ 2) + b1 * 256 + b0, 这样将256 = 255 + 1 代入,上式可以写成255 * I + b7 + b6 + b5 + b4 + b3 + b2 + b1 + b0 对此求余,就是我们要得到的1的个数,即b7 + b6 + b5 + b4 + b3 + b2 + b1 + b0。
MIT HACKMEM Count
public static int bitCount_MIT(long n) {
// HD, Figure 5-14
n = n - ((n >>> 1) & 0x5555555555555555L);
n = (n & 0x3333333333333333L) + ((n >>> 2) & 0x3333333333333333L);
n = (n + (n >>> 4)) & 0x0f0f0f0f0f0f0f0fL;
n = n + (n >>> 8);
n = n + (n >>> 16);
n = n + (n >>> 32);
return (int)n & 0x7f;
}
这是jdk中的源码,形式和32位的算法有点不一样,这里n = n - ((n >>> 1) & 0x5555555555555555L),相当于n = (n & MASK_1) + ((n >>> 1) & MASK_1) ,只是少了一次与操作的开销,后面跟Parallel Count类似。
Neat Count
private final long SHIFT_256 = 0x0101010101010101L;
public int bitCount_Neat(long n){
n = n - ((n >>> 1) & MASK_1);
n = (n & MASK_2) + ((n >>> 2) & MASK_2);
n = (n + (n >>> 4)) & MASK_4;
return (int)((n * SHIFT_256) >>> 56);
}
比较简洁,效率要看64位乘的开销,相对于上面的MIT HACKMEM Count如果一次乘操作比2次位操作和3次加操作的开销小的话,就能得到一定的提高。一般的,java中对long数据操作时,乘法开销与加减,位操作的开销相比,没有在int数据操作时差距那么大。对于最后一步乘操作,可以将SHIFT_256拆开来看,1 + 100 + 10000......这样,n * SHIFT_256 >> 56 就可以看作n >> 56 + (n << 8) >> 56 + (n << 16 ) >> 56 ......其实就是每次把8位中的1的个数相加。
/* MCPS = Million counts per second Total Count = 100 million
Loop Time(ms) Compute time(ms)
* Loop time Compute time MCPS
*bitCount_Neat 469 2249 44
*bitCount_MIT 484 2844 35
*bitCount_Nifty 469 5797 17
*bitCount_Parallel 468 3579 27
*bitCount_Precomput16 453 1531 65
*bitCount_Precomput8 469 2781 35
*bitCount_Dense 469 31109 3
*bitCount_Sparse 485 9718 10
*bitCount_Iterated 469 13031 7*/
bitCount_Dense 的效率在平均情况下,应该和bitCount_Sparse相当,在此可能是因为我的测试数据是从0-100000000之间的缘故,100000000=5F5E100 也就用了28位。从表中可以看出,16位查表的方法是最快,但是需要64k的空间来存储查询的表。另外比较bitCount_Neat和bitCount_Nifty ,可以看出%操作的开销是很大的。bitCount_Neat和bitCount_MIT比较,可以看出乘法操作的开销不是很大。