计算二进制数中1的个数

1、逐位遍历

public int bitCount1(int n){
    int count = 0;
    while(n != 0){
        count += (n & 1);
        n >>>= 1;
    }
    return count;
}

原理: 每个数与1的结果就是该数最后一位上1的个数。

2、跳过不必要的0

public int bitCount2(int n){
    int count = 0;
    while (n != 0){
        count++;
        n = (n) & (n-1);
    }
    return count;
}

原理: 一个数n 与 (n-1)的结果是将n的最低位(最右边)的那个1变为0。这样每做一次与操作,n中就少一个1,直至最后得到0。比方法1的改进之处在于,只需要遍历1,0被直接跳过。

3、分治法

public int bitCount2(int n){
    n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);
    //加减优先于位移优先于位
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
    n = (n & 0x0F0F0F0F) + ((n >>> 4) & 0x0F0F0F0F);
    n = (n & 0x00FF00FF) + ((n >>> 8) & 0x00FF00FF);
    n = (n & 0x0000FFFF) + ((n >>> 16) & 0x0000FFFF);
    return n;
}

16进制数对应的2进制数:

0x55555555 = 01010101010101010101010101010101
0x33333333 = 00110011001100110011001100110011
0x0F0F0F0F = 00001111000011110000111100001111
0x00FF00FF = 00000000111111110000000011111111
0x0000FFFF = 00000000000000001111111111111111

原理: 分治法。

  1. 先将n视为32个单独的位,然后分别计算每一个位上1的个数;
  2. 将32个位两两合并,得到16组两两合并后的1个数;
  3. 然后将16组两两合并,得到8组,每组都是4个位中1的个数;
  4. 依次类推…
  5. 最后将2组合并,得到最终的32位中1的个数。

在这里插入图片描述
如上图所示,通过类似于归并排序的方法,两两合并,最终得到整体的1的个数。
如何合并呢?举例说明(只用8位):

  1. n = 0b01001110,两两一组可以分为4组:01-00-11-10;
  2. 计算第一组(0b01)中1的个数为:0 + 1 = 0b01,即(0b01 & 0b01) + ((0b01 >>> 1) & 0b01),也就是说,将第一位上1的个数加上第2位上1的个数,同理第2组(0b00)中1的个数为0b00个,第3组(0b11)中1的个数为0b10个,第4组(0b10)中有0b01个,因此归并一次之后得到0b01001001
  3. 将上一步的结果拆分为两组:0100-1001,对第1组(0b0100)进行归并,也即将0b010b00相加得到0b0001,计算方法为(0b0100 & 0b0011) + ((0b0100 >>> 2) & 0b0011),同理将第2组归并,0b10 + 0b01 = 0b0011,也即有3个1,这一轮我们可以得到0b00010011
  4. 这是最后一步,将0b0001+0b0011得到0b00000100,即一共有4个1,计算方法为(0b00010011 & 0b00001111) + ((0b00010011 >>> 4) & 0b00001111)

4. JAVA官方的方法

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

上面的代码是Integer类里面bit位为1的个数计算的实现方法。看起来和上面的分治法有点像,又不一样,其实它脱了马甲还是分治法的身体,就是java开发者为了炫技穿的那一层花里胡哨的大裤衩而已。下面咱们来一行一行分析:

  1. i = i - ((i >>> 1) & 0x55555555);
    减号右边是将i无符号右移一位并 & 0101...将奇数位保留,实际上就是将i的偶数位移动到奇数位上(原来的奇数位全部变为0),然后用i减去这个数,得到的结果其实是和归并一次后的结果一样的,即组(每两个bit为一组)内1的个数。为什么是这样,我们来看一下,既然是2bit为一组,组与组都是一样的,那么我们只分析一组就行了,分四种情况:00011011,下面的图是结果
四种情况:          	00   01   10   11
偶数位移到奇数位后: 	00   00   01   01
相减的结果:        	00   01   01   10

可以看到最后的结果正好是第一行中1的个数。这里也可以看出为什么组与组之间都是相同的,只用分析一组即可,因为在做减法的时候并没有出现不够减的情况,不会向高位借1,因此组与组之间互不影响。
2. i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
和上面的分治法一样。
3. i = (i + (i >>> 4)) & 0x0f0f0f0f;
这里和分治法的代码有点区别,是先将自己与自己右移4位的结果相加,然后再进行与操作的。我们看看效果:
为了方便理解起见,下面的字符只起到一个划分bit位的作用,不代表真正的16进制数的运算,中间的是分隔符不是减号。
假设i为6-7-8-9-A-B-C-D
无符号右移4位:0-6-7-8-9-A-B-C
两者相加:(6+0)-(7+6)-(8+7)-(9+8)-(A+9)-(B+A)-(C+B)-(D+C),其中每个括号表示一个4bit二进制数。
首先我们考虑这样一件事,即4bit可以表示的最大的数是多少,是不是15?
其次,8bit最多有几个1,是不是8个?也就是说8bit中1的个数只需要4bit就能表示了。
我们来看括号(D+C)里面的数,D表示4bit中1的个数,C也表示4bit中1的个数,最多也就8个,用4bit的空间足够表示了,并不会溢出。
而我们再看两者相加的结果,其中奇数(从右往左,从低位到高位数)括号中不正是我们要的归并结果吗?又经过分析,可知结果并不会溢出,因此我们可以毫无顾忌的将偶数位括号中的结果都抹除了。所以 & 一下0x0f0f0f0f搞定。
4. i = i + (i >>> 8);
到这里又不一样了,别慌,还是用例子说话。经过上面三行操作,我们已经知道了,现在我们已经得到了每8bit一组的组内1的个数,现在需要的就是将这四个组再来一次归并,得到16bit一组的组内1的个数。
由3我们可知我们得到的数应该是这样的:0A-0B-0C-0D,即每8bit一组,表示该8bit中1的个数,并且这8bit的高4位是全0,原因上面已经说了,8bit最多8个1,只需要低4位就可以表示。
无符号右移8位,得到00-0A-0B-0C
二者相加得到:(A+0)-(B+A)-(C+B)-(D+C)
根据3中的分析,我们需要的是奇数括号(现在每个括号代表8bit)里的值,但是他没有 像上一步那样 & 0x00ff00ff。我们接着往下看:
5. i = i + (i >>> 16);
为了方便,我们将上面得到的结果(A+0)-(B+A)-(C+B)-(D+C)表示为:H-J-K-L,其中每一个字符代表8bit,J和L里面存了我们要的结果。
无符号右移16位得到:0-0-H-J
两者相加得到:(H)-(J)-(K+H)-(L+J),可以看到我们只需要第一个括号(从低位到高位数)(L+J) 保存的结果,这个括号有8bit空间。
6. return i & 0x3f;
因此我们只需要将其 & 0xff即可。但是它明明与的是0x3f啊。哈哈,还记得大明湖畔的夏雨荷吗,就是上面3里面分析的啊,想想看,32bit最多有几个1呢,需要多少bit来表示它呢?是不是仅需6bit啊,因此低8位里实际上有两位并没有用到,是空的。你可能会说,这是有符号数啊,带上符号位要七位,但是符号位在第32位呢,经过&操作,第32位已经是0了,它就表示的正数。
花了我一个下午,写这篇文章,只想说那些穿花裤衩的,你够了啊,做个人吧,秀了别人一脸。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值