深入解析Integer.bitCount()源码

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位足够了。


一起坚持刷题打卡,欢迎你加入:力扣神秘刷题交流群

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值