本文讨论内容基于Jdk1.7
bitCount实现的功能是计算一个(byte,short,char,int统一按照int方法计算)int,long类型的数值在二进制下“1”的数量。
网上关于此方法的解释已经不少,但是浏览下来包括stackOverFlow在内没有发现讲的特别容易理解的,我暂且试试将这个方法的实现讲的通俗易懂,如果小伙伴看过之后能在某一时刻回忆起来还很清晰也不失为功德一件,但我的表达能力有限,希望读者可以提出宝贵意见。
Jdk计算int类型bit数量的源代码,long型的大同小异:
public static int bitCount(int i) {
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;
}
计算bitCount的思想总体上分两个阶段:
1、计算每两位中1的数量,将原来表示数值的二进制数变成每两位表示这两位中“1”数量的二进制数。
2、将这些数量加在一起。
阶段2的实现,用一句话就是把二进制数按两位分组,相邻分组两两相加得四位二进制的bitCount,再按四位分组,相邻分组两两相得八位二进制的bitCount,以此类推直到算出32位的bitCount数量。
源码中用到的十六进制和二进制的关系:
十六进制 | 二进制 |
---|---|
0x55555555 | 01010101010101010101010101010101 |
0x33333333 | 00110011001100110011001100110011 |
0x0f0f0f0f | 00001111000011110000111100001111 |
第一行代码执行完毕后i的值所表示的二进制数表达的已经是每两位一个二进制数,每个二进制数的值表示这两位中“1”的数量。
第二行代码将二进制数按两位分组,赋值语句右侧的前半部分(i & 0x33333333)将二进制数第1、3、5、7、9…15组两位二进制保留原位,其他位归零,后半部分将二进制数右移两位后将原二进制数的第2、4、6、8、10..16组两位二进制数放到第1、3、5、7、9….15组两位二进数位置上,目的是将他们相加后,使原二进制数的两位二进制组第1、2合并,3、4合并,5、6合并…15、16合并,计算后i的值所表示的二进制数表达的是每四位一个四位二进制数,每个四位二进制数的值表示这四位中“1”的数量。
第三行代码将二进制数按四位分组,思路和上面基本一样,只是要注意
(i + (i >>> 4)) & 0x0f0f0f0f 不等价于 (i & 0x0f0f0f0f ) + ((i >>> 4) & 0x0f0f0f0f )
但因为四位二进制可以表示最大15的数值,计算后用后四位表示一个八位的“1”数量足够,这样又可以少一次按位与运算,在上一步的时候两位二进制不足以表示四位二进制数的“1”的数量,为了效率,采用(i & 0x33333333) + ((i >>> 2) & 0x33333333)这种方式。计算后i的值所表示的二进制数表达的是每八位一个八位二进制数,每个八位二进制数的值表示这八位中“1”的数量,并且每个八位二进制数的前四位必然是0。
第四行、第五行只需要关注二进制的后六位就可以了,因为32位int值不可能超过2的6次方(64)个“1”,最后在return语句中将第四行、第五行代码的垃圾数据过滤掉只保留最后6位,还是为了效率。
阶段1的实现有两种思路:
1、Jdk源码所表现出来的思路:
结论:若有一个二进制数,他的“1”的数量等于二进制数本身 - 二进制高位上是0?0:1 。
验证:
00 - 0 = 0;
01 - 0 = 1;
10 - 1 = 1;
11 - 1 = 10 = 2;
显然是正确的,如果读者认为这个理解起来已经很直观,那下面的内容可以略过。
2、作者认为Jdk源码的思路不是很好理解,虽然Jdk源码的实现是效率最高的,但是首先需要做到理解再想一下如何提交嘛,说一下作者的思路:
结论:若有一个二进制数,他的“1”的数量等于 个位数字+高位数字。
验证:
00 个位数字 0 高位数字 0 bit数 = 0+0 = 0
01 个位数字 1 高位数字 0 bit数 = 1+0 = 1
10 个位数字 0 高位数字 1 bit数 = 0+1 = 1
11 个位数字 1 高位数字 1 bit数 = 1+1 = 2
显然也是成立的。
按照作者的思路,将源码做如下转换(注,第一行代码效率要比Jdk源码低,只是为了方便理解):
public static int bitCount(int i) {
i = ((i >>> 1) & 0x55555555) + (i& 0x55555555);
i = ((i >>> 2) & 0x33333333) + (i & 0x33333333);
i = ((i >>> 4) + i) & 0x0f0f0f0f;
i = (i >>> 8) + i;
i = (i >>> 16) + i;
return i & 0x3f;
}
是否注意到我将第一行的运算符改成+
,并且总是将移位运算放在前面,下面说明为什么。
如何计算两位的二进制数中“1”的数量?分两步:
名词定义:两位的二进制数上从右到左第一位叫【个位】,第二位叫【二位】(沿用十进制叫法)。
1、【个位】为0或者1就代表个位上“1”的数量,这个毫无疑问。
2、【二位】上的值右移1位,原【个位】直接溢出舍弃,则移位后【个位】即原【二位】,为0或者1即代表原两位二进制数的【二位】上“1”的数量。
以上两步的和就是这个二位二进制数的bitCount。
用伪代码描述:
已知:& 是按位与,二进制数按位与01即取【个位】上的值。
1、a的个位数 a&01
2、a的二位数 a>>1&01
得:bitCount = (a&01) + ((a>>>1)&01) ; 注—-‘&’优先级低于’+’,所以要用括号
如上所述第一行代码变成和下面统一的+
连接。
另:将移位运算符都放在前面的意义是每一次运算其实都是将高位分组合并到低位分组得出这个分组的总数量。