JDK中,Integer#bitCount()
方法的源代码如下:
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;
}
该方法巧妙运用了分治和归并的思想,通过很少的几步位运算就能计算出了一个整型值,其二进制表示形式中为1的位的个数。
可能一般情况下没有多少人会去深究这个代码实现,咋看之下会觉得一头雾水。
接下来,我们从一道LeetCode题目开始,步步深入,尝试揭开java.lang.Integer#bitCount()
方法的神秘面纱。
题目
LeetCode #191题:给定一个32位整数n,返回其二进制表达形式中 ‘1’ 的个数(也被称为汉明重量)。
这道题目有多种解法,最简单的就是调用Integer#bitCount()
直接返回结果。
下面先给出几种常见的解法,最后再探讨java.lang.Integer#bitCount()
的实现原理。
方法1
从低位开始,逐个二进制位进行判断,最多需要判断32次。
public static int bitCount1(int n) {
int count = 0;
while (n != 0) {
count += (n & 1);
n >>>= 1;
}
return count;
}
方法2
利用 n=n&(n-1)
可以将n的最低位的1抹掉的性质。判断次数跟二进制形式中1的个数相同。
public static int bitCount2(int n) {
int count = 0;
while (n != 0) {//不为0 说明二进制形式中有1
n &= n - 1;//将最低位的1抹掉
count++;
}
return count;
}
方法3
缓存4个二进制位可以表示的[0,15]
这16个数中,各自二进制形式中1的个数。32位整数的每4位为一组进行统计。统计次数为8次。但是需要额外16个byte
的存储空间。
public static int bitCount3(int n) {
// 缓存4个二进制位可以表示的0~15中1的位数
byte[] cache = new byte[]{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
byte count = 0;
while (n != 0) {
count += cache[n & 0x0f];
n >>>= 4;
}
return count;
}
上面都是一些常规解法,跟java.lang.Integer#bitCount()
的实现方式相差还很远。
下面开始给出的方法都是时间复杂度为O(log32)
的解法,可以理解为“两两合并”、“二路归并”的思想。
方法4
第一步:每2位内部合并一个结果;
第二步:每4位内部合并一个结果;
第三步:每8位内部合并一个结果;
第四步:每16位内部合并一个结果;
最后:每32位内部合并一个结果,得到答案。
/**
* 核心思想是对1的个数进行两两合并。
* 以8bit,并且8个bit都是1为例:
* 1 1 1 1 1 1 1 1
* 0 2 0 2 0 2 0 2 第1次,每2位内部合并
* 0 0 0 4 0 0 0 4 第2次,每4位内部合并
* 0 0 0 0 0 0 0 8 第3次,每8位内部合并,得到最终结果8
* 如果是32位,就需要每2/4/8/16/32位内部合并一次,总共合并5次
*/
public static int bitCount4(int n) {
// 0xaaaaaaaa + 0x55555555 = 0xffffffff
// n & 0xaaaaaaaa保留n中每相邻两位的高位
// n & 0x55555555保留n中每相邻两位的低位
n = (n & 0x55555555) + ((n & 0xaaaaaaaa) >>> 1);
n = (n & 0x33333333) + ((n & 0xcccccccc) >>> 2);
n = (n & 0x0f0f0f0f) + ((n & 0xf0f0f0f0) >>> 4);
n = (n & 0x00ff00ff) + ((n & 0xff00ff00) >>> 8);
n = (n & 0x0000ffff) + ((n & 0xffff0000) >>> 16);
return n;
}
上面这个实现,好好理解。这个是基础。下面我们就开始修改这份代码实现,逐步跟Integer.bitCount()
的源码靠拢。
方法5
/**
* 方法4的改进版,少用了5个int常数。
*/
public static int bitCount5(int n) {
// n & 0x55555555取n中每相邻两位的低位
// (n>>>1) & 0x55555555取n中每相邻两位的高位
n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);//1
n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);//2
n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);//3
n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);//4
n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff);//5
return n;
}
方法6
究极方案,亦即java.lang.Integer#bitCount()
的实现方式。
/**
* 每一行都与上面bitCount4的版本等效,但是用到的常数和位运算次数更少
*/
public static int bitCount6(int n) {
n = n - ((n >>> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
n = (n + (n >>> 4)) & 0x0f0f0f0f;
n = n + (n >>> 8);
n = n + (n >>> 16);
return n & 0x3f;
}
有了方法5的铺垫,看懂方法6应该不是太难,但是其中细节还挺多的,接下来详细分析:
- 第1行:跟方法5等价。推导如下:
// 注意下面的推导没有考虑运算符的优先级,通过空格来区别优先级
n&(0x55555555 + 0xaaaaaaaa) == n
n&0x55555555 + n&0xaaaaaaaa == n
n&0x55555555 + 2[(n&0xaaaaaaaa)>>>1] == n
n&0x55555555 + (n&0xaaaaaaaa)>>>1 == n - (n&0xaaaaaaaa)>>>1
//因此:
n&0x55555555 + ((n>>>1)&0x55555555) == n - ((n>>>1)&0x55555555)
-
第2行:相同。
-
第3行:方法5中第三行公共的
0x0f0f0f0f
提出来即得:(n + (n >>> 4)) & 0x0f0f0f0f
。
思考:为什么方法5中前两行不可以把公共的
0x55555555/0x33333333
提出来呢?应该是,这时n还比较大,提出公共部分后,
n+(n>>>1)
可能溢出int
的表示范围。
- 第4行:同样是先把公共的
0x00ff00ff
提出来,但是为什么又省掉了&0x00ff00ff
了?
n + (n >>> 8)
按位与上0x00ff00ff
的目的是让相应的高位变成0,其实这时候高位变不变为0已经不重要了,因为我们只关心4个f的低位,
这4个f的位置加在一起就是最后的答案(答案只须取n最后的一个字节)。
-
第5行:分析同上。
-
第6行:返回值可以改为等价的
n & 0xff
,只是int
中最多32个1,取低6位足够了。
一起坚持刷题打卡,欢迎你加入:力扣神秘刷题交流群