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
原理: 分治法。
- 先将n视为32个单独的位,然后分别计算每一个位上1的个数;
- 将32个位两两合并,得到16组两两合并后的1个数;
- 然后将16组两两合并,得到8组,每组都是4个位中1的个数;
- 依次类推…
- 最后将2组合并,得到最终的32位中1的个数。
如上图所示,通过类似于归并排序的方法,两两合并,最终得到整体的1的个数。
如何合并呢?举例说明(只用8位):
- 设
n = 0b01001110
,两两一组可以分为4组:01-00-11-10
; - 计算第一组(
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
; - 将上一步的结果拆分为两组:
0100-1001
,对第1组(0b0100
)进行归并,也即将0b01
与0b00
相加得到0b0001
,计算方法为(0b0100 & 0b0011) + ((0b0100 >>> 2) & 0b0011)
,同理将第2组归并,0b10
+0b01
=0b0011
,也即有3个1,这一轮我们可以得到0b00010011
; - 这是最后一步,将
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开发者为了炫技穿的那一层花里胡哨的大裤衩而已。下面咱们来一行一行分析:
i = i - ((i >>> 1) & 0x55555555);
减号右边是将i无符号右移一位并 &0101...
将奇数位保留,实际上就是将i的偶数位移动到奇数位上(原来的奇数位全部变为0),然后用i减去这个数,得到的结果其实是和归并一次后的结果一样的,即组(每两个bit为一组)内1的个数。为什么是这样,我们来看一下,既然是2bit为一组,组与组都是一样的,那么我们只分析一组就行了,分四种情况:00
,01
,10
,11
,下面的图是结果
四种情况: 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了,它就表示的正数。
花了我一个下午,写这篇文章,只想说那些穿花裤衩的,你够了啊,做个人吧,秀了别人一脸。