引入
今天在使用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的个数 |
---|---|
00 | 00 |
01 | 01 |
10 | 01 |
11 | 10 |
也就是说我们要将这个int值每两位左列所示的二进制位,变成右列所示的二进制位。
将左列标记为s,右列标记为t,则t = s - x,观察上述表格可以发现x = s >>> 1,即
t = s - (s >>> 1);
s | t | x |
---|---|---|
00 | 00 | 00 |
01 | 01 | 00 |
10 | 01 | 01 |
11 | 10 | 01 |
不止两位
上述我们讨论了只有两个二进制位的情况,如果对于整个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,根据这个原理我们可以得到下表,可以对照着这个表格加深理解,
s | t |
---|---|
0000 | 0000 |
0001 | 0001 |
0010 | 0010 |
0100 | 0001 |
0101 | 0010 |
0110 | 0011 |
1000 | 0010 |
1001 | 0011 |
1010 | 0100 |
我们最终的到的是个四位数,但是我们只需要两位数进行求和运算,所以为了消除高位的影响,我们需要与上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;
总结
要理解这个算法,主要是要理解两位数和四位数这里,后面的几位数与四位数那个道理是一样的,其中重点理解与上一个数来消除高位影响。