(转)位计算的诸多算法(计算二进制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比较,可以看出乘法操作的开销不是很大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值