Java Integer.bitCount()方法解析

引入

今天在使用Integer类的时候点进去看它的源码,发现了bitCount()这么一个方法,看它的介绍是用来获取一个int中二进制位为1的个数。然后看了它的实现,完全看不懂的节奏啊。

public static int bitCount(int i) {
    // HD, Figure 5-2
    i = i - ((i >>> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
    i = (i + (i >>> 4)) & 0x0f0f0f0f;
    i = i + (i >>> 8);
    i = i + (i >>> 16);
    return i & 0x3f;
}

这个方法如果我自己来实现的的话,会是下面这个样子:

public static int bitCount(int i) {
    int num = 0;
    for (int j = 0; j < 32; i++) {
        num += (i) & 1;
        i >>>= 1;
    }
    return num;
}

为了搞清楚这个方法背后的原理,查了一些资料,终于搞明白了。

分析

算法思路

这个算法的主要思路是这样的:

  • 求每两位二进制数中1的个数
  • 求每四位二进制数中1的个数
  • 依次类推,求到三十二位二进制数中1的个数,就是我们需要的答案

两位

只有两位

首先来分析求两位怎么求:
两位二进制数有四种可能,它们所对应的1的个数(在这里个数也使用二进制来表示)如下所示,

二进制数二进制位为1的个数
0000
0101
1001
1110

也就是说我们要将这个int值每两位左列所示的二进制位,变成右列所示的二进制位。

将左列标记为s,右列标记为t,则t = s - x,观察上述表格可以发现x = s >>> 1,即
t = s - (s >>> 1);

stx
000000
010100
100101
111001

不止两位

上述我们讨论了只有两个二进制位的情况,如果对于整个int32位来说,转化关系是怎样的呢?

在这之前先来讨论4位的情况,举个例子说吧:

  • 现在有0111这个数,我们要求每两位的1的个数,目标值是0110,如果直接代入上述公式,我们得到的是0100。

上述结果明显不对,但是哪里错了呢?来分析一下:

  • 代入公式的运算过程
    • 0111 >>> 1 = 0011 ①
    • 0111 - 0011 = 0100 ②
  • 理想运算过程
    • 0111 - 0001 = 0110 ③

对比可知道上面第一步求得的0011是不对的,也就是说我们不能直接右移一位得到减数,那么应该怎么做?

仔细分析可以发现,减数之间的区别在于右移时从高位传到低位的那个1,根据表格我们知道x(x代表减数)的高位不可能是1,而1又会因为右移从高位传过来,所以在右移之后,我们需要与上0101来消除这个高位影响。将这个扩展到32位二进制数上,我们就得到公式i = i - ((i >>> 1) & 0x55555555);

第一步搞定了。

四位

再来看四位的求法。
这里我们需要注意一点,此时的二进制位已经不再仅仅代表二进制位,我们应该把两位二进制位看成一个整体,当成个数来看。也就是说,我们需要对这四位数进行拆分,拆成两组,每组分别代表这两位包含的1二进制位的个数。那么这4个二进制位上面所包含的所有为1的二进制位的个数为两组的和,也就是高位所代表的二进制数与低位所代表的二进制数的和。比如有这样一个4位的二进制数,abcd,它所包含的1位的个数为,00ab+00cd,注意前面的两个0,根据这个原理我们可以得到下表,可以对照着这个表格加深理解,

st
00000000
00010001
00100010
01000001
01010010
01100011
10000010
10010011
10100100

我们最终的到的是个四位数,但是我们只需要两位数进行求和运算,所以为了消除高位的影响,我们需要与上0011,那么就得到第二个公式i = (i & 0x33333333) + ((i>>>2) & 0x33333333;。这里要讲一下,为什么0x33333333要&两次,而不是放在外面一起&。这是因为00ab&00cd可能会产生进位,这样的话,如果放在外面与就会消除这个进位,使结果不正确。

这一步理解之后,后面就很快了。

八位、十六位、三十二位

这几个位数与四位时的原理是一样的。

  • 八位:向右移四位,与原数相加,因为这里四位中最大的可能数为0100,产生进位也只是1000,不会影响到前面的4位数,所以八位的时候可以在外面&,得到公式i = (i + (i >>> 4)) & 0x0f0f0f0f;
  • 十六位:i = (i + (i >>> 8)) & 0x00ff00ff;
  • 三十二位:i = (i + (i >>> 16)) & 0x0000ffff;

我这里表达形式稍微与源码有点不一样,源码是把消除部分放到了最后,即i = i & 0x3f;,而省略了十六位和三十二位那里的与操作,因为最后面二进制位为1的个数不可能超过100000这个二进制数,所以这两步与操作可以直接放到最后用一步来代替,最终我们就得到所有的公式:

i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
i = i & 0x3f;

总结

要理解这个算法,主要是要理解两位数和四位数这里,后面的几位数与四位数那个道理是一样的,其中重点理解与上一个数来消除高位影响。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值