Integer.bitCount (int i)源码剖析

前言

最近在刷力扣题时,刷到了一道统计数字二进制位里面1的数量的题,用常规方法做出来后,一看评论区才知道原来java的Integer类自带统计数字二进制表示里面1数量的方法——Integer.bitCount (int i)。但是Integer.bitCount (int i)的源码长得非常古怪,出于兴趣,我对它的源码好好研究了一番,并且特地在此记录。

这是Integer.bitCount (int i)的源码,后面会对这个源码进行详细解析:

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;
}

预备知识

首先在解析源码之前,需要对位运算和补码的知识有一定的了解。如果你对位运算和补码已经非常了解,可以跳过这一节。

这里用到的位运算只有两个&>>>

位与运算&

位与运算是对两个操作数按位取与。即只有两个操作数的第n位均为1时,结果的第n位才为1;两个操作数有一个的第n位为零,则结果的第n位为零。

例如,3&5==1:
3:   0B 0000 0000 0000 0000 0000 0000 0000 0011
5:   0B 0000 0000 0000 0000 0000 0000 0000 0101
3&5:  0B 0000 0000 0000 0000 0000 0000 0000 0001

根据位与运算的特点,我们可以将一个数字的某些位 置零,例如:
将31的低4位 置零:
31:    0B 0000 0000 0000 0000 0000 0000 0001 1111
-16:   0B 1111  1111 11111 1111 1111  1111 1111 0000
&:    0B 0000 0000 0000 0000 0000 0000 0001 0000

无符号右移>>>

java中的移位运算符有三种左移(<<)、右移(>>)和无符号右移(>>>)。 左移就是将二进制数左移指定位数,高位丢弃、低位补零

右移是将二进制数右移指定位数,低位丢弃,但是高位补数这里有一点不同。我们知道java中的数字都是以补码形式存储的,正数的补码是其本身,但是负数的补码是其对应正数的原码的每一位取反结果再加一,其中最高位是符号位,正数的符号为0,负数为1。

对一个数进行右移运算时,低位丢弃,高位补的是它的符号位的值,也就是说正数的高位补0、负数的高位补1。

根据上面的内容我们知道右移是与符号相关的,而无符号右移就是剔除了右移的符号相关性,无论是正数还是负数,执行无符号右移后,高位都补零

根据右移运算的特点,右移一位就相当于将原数除以2并且向下取整,15 >> 1 == 7-15 >> 1 == -8

无符号左移一位,如果是正数或者无符号数,则跟右移同效。对于负数,结果是其所对应正数除以2并且向下取整后的数字的补数,即n >>> 1 == nteger.MAX_VALUE - (-n >> 2),仅n为负数时成立!,至于为什么这样,跟补码表示有关,这里就不展开讲了。

三者区别如下:

移位方向补数
左移向左低位补零
右移向右高位补符号位的值
无符号右移向右高位补零

需要注意的是,位运算是直接对二进制位的操作,源码补码这些只是对一个二进制串解释方法的不同,不会改变这个二进制串本身。

补码

对于源码补码的知识,大家可参阅我的另一篇文章:原码、反码与补码

源码讲解

基本原理

我们知道一个二进制数的某一位的值只可能是0或者1,我们可以换一种理解方式,可以将零或者1理解为这个数字在这一位上1的数量,为零表示这一位没有1,为零表示这一位有一个1,基于这种理解,我们将这个二进制数每一位上的值(0或1)加起来,就是这个数的各个二进制位上1的数量,好好理解这句话后面有大用。

那么问题来了,如何将这个数每一位的值相加呢?
两位二进制

先考虑只有两位的情况,例如 n=0B11

首先请大家思考下对于两位十进制数n,我们是如何计算它的十分位和个分位的和的,是不是count = n/10 + n%10

那么对于两位二进制就可以写成count = n/2 + n%2。除以2是为了将低位的值舍弃,再将高位的值提取出来放到低位,这一步可以用n >>> 1替代,执行时低位自动被舍弃;取余2是为了消除高位的值,或者说将高位 置零,可以用n & 0B01替代。那么就可以改写成count = (n >>> 1) + (n & 0B01)

四位二进制

现在考虑4位的情况。以n = 0B1011为例。有了前面的基础,我们可以先将n两两分组,每组两位,n = 0B 10 11,利用前面两位的计算方法,将每一组的高位和低位相加,再将这两组的值加起来就是最终结果。

上面是位与上0B01,这里有两组,所以要位与上0B 01 01,即n1 = n & 0B0101,这样n1每一组的值就是n每一组低位里面1的数量。

前面是左移一位,因为低位的值会被自动丢弃。但是这里有两组,直接右移会将第一组低位的值放到第二组的高位,影响后续计算,所以执行>>1将高位放到低位后要再将每一组的高位 置零,即n2 = (n >>> 1) & 0B0101 ,这样n2每一组的值,就是n对应组的高位里面1的数量。

n3 = n1 + n2,现在n3的每一个分组的值就是n的每一个分组里面1的数量。将两个分组的值相加,过程与前面相同,现将n3的低分组置零提取出高位分组,再右移两位,n4 = (n3 & 0B1100) >> 2,的到高分组的值,只有两个分组,右移两位低分组的值被自动舍弃,所以简写成n4 = n3 >> 2;将n3的高位 置零,n5 = n3 & 0B0011,得到低位分组的值,n4 + n5就是n的所有位里面1的数量。

大家可以在草稿本上比划一下这个过程,说起来很复杂,其实写起来挺简单的。
32位的int

现在我们就可以通过上面的方式,求任何位数为2的幂的二进制数里面1的数量,对于java里面32位的int也不例外,并且只需要经过 log ⁡ 2 32 \log_{2}{32} log232=5次上述过程即可。可以看出上面的过程其实就是一种分治的思想。

但是有一点要注意,java里面32位的int是有符号的,最高位为符号位,这也是为什么一直用的是无符号位移>>>而非 有符号位移>>。对于补码源码,对这个问题没有影响,因为为源码补码只是这个二进制串的整体所表示值的不同,对它本身每一位的值没有影响。

源码详解

源码:

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;
}

首先,
0x55555555 == 0B 0101 0101 0101 0101 0101 0101 0101 0101
0x33333333 == 0B 0011 0011 0011 0011 0011 0011 0011 0011
0x0f0f0f0f == 0B 0000 1111 0000 1111 0000 1111 0000 1111

按照之前的思路,第一步应该是将32的int两位一组,分成16个分组,将每个分组高位的值与低位的值相加得出该分组里面1的数量。

(i >>> 1) & 0x55555555这句话的作用很明确,就是将每个分组高位的值提取出来,可是i = i - ((i >>> 1) & 0x55555555);中间为什么是减号?

我们假设n为某一个分组的值,n有两个二进制位,假定两个二进制位的值分别为x,y,则n = 2x + y,那么n - x == x + y,即高位与低位之和。

上面(i >>> 1) & 0x55555555相当于是求出了x,所以i = i - ((i >>> 1) & 0x55555555);i = (i & 0x55555555) + ((i >>> 1) & 0x55555555);等价。

第二句话就是常规操作了,i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);,将16个分组分成8组,每组的高位分组与低位分组值相加,就是每组(4位)里面1的数量。

第三句话又张的不一样了,i = (i + (i >>> 4)) & 0x0f0f0f0f;。我们知道现在有8个分组,每个分组4位,8个分组两两一组后为4个组,每组八位最多有8个1,而低分组的4位可存储的最大值为15>8,不会溢出。所以可以先加再置零。

第四句话是一样的道理,i = i + (i >>> 8);,4个分组分成两组,低分组的八位足以容纳32,所以不必在将第一组置零。

第五句话跟第四句话一样,不再过多解释。

最后一句话,取低6位,五位最大31,六位最大63。

总结

可以看出jdk源码对上面的算法做了不少细节上的优化,这需要对位运算有极高的理解,将每一步都优化到极致,短短的六句话就蕴含了这么多细节。相信将这个源码完全看懂后,你会对位运算和二进制有更加深刻的理解。

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值