思考这个问题的起因是看到了力扣191题的解法,最开始我是直接取每位二进制位累加1的个数。
起先看不太懂hamingWeight的做法是在干嘛,后来得知似乎是jdk源码的实现逻辑,看了一些文章,自己做了一些总结。
以下将以这段代码为例进行详细解释:
public int bitCount(int i) {
i = i - ((i >>> 1) & 0x55555555); // 1
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); // 2
i = (i + (i >>> 4)) & 0x0f0f0f0f; // 3
i = i + (i >>> 8); // 4
i = i + (i >>> 16); // 5
return i & 0x3f;
}
一、每2位存储1的个数
只需将这2位数-高位上的值,具体如下:
二进制位 | 1的个数(十进制) | 1的个数(二进制) |
11 = 3 | 3 - 1 = 2 | 10 |
10 = 2 | 2 - 1 = 1 | 01 |
01 = 1 | 1 - 0 = 1 | 01 |
00 = 0 | 0 - 0 = 0 | 00 |
i = i - ((i >>> 1) & 0x55555555); // 1
(i >>> 1) & 0x55555555:这部分的作用是将2位中高位的值获取到低位,高位为0。
i = i - ((i >>> 1) & 0x55555555):每2位都减去高位上的值,也就是上面表格所说的-1,这样每2位就存储了2位中1的个数。
以1111 1111 1111 1111 1111 1111 1111 1101为例
11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 01 |
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 01 |
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 |
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); // 2
这部分代码的作用是将每2位存储的1的个数存在4位中,也就是把2位+2位为一组。
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 |
2位+2位为一组 |
4 4 4 4 4 4 4 3 |
i = (i + (i >>> 4)) & 0x0f0f0f0f; // 3
同理,这部分代码的作用是将每4位存储的1的个数存在8位中,也就是把4位+4位为一组。
4 4 4 4 4 4 4 3 |
4位+4位为一组 |
8 8 8 7(8位) |
i = i + (i >>> 8); // 4
这部分的代码是将相邻的8位中1的个数聚集在一起
8 8 8 7(8位) |
相邻的8位中1的个数聚集在一起 |
8 16 16 15 |
i = i + (i >>> 16); // 5
这部分的代码是将相邻的16位中1的个数聚集在一起
8 16 16 15 |
将相邻的16位中1的个数聚集在一起 |
8 16 24 31 |
最后的低6位也就是二进制位中1的个数。
这个地方可能有一个疑问,就是为什么>>>8和>>>16的时候不需要做与操作呢?
不妨设>>>8之前每8位,分别为D4、D3、D2、D1,Di表示第i个8位中1的个数。
D4 D3 D2 D1 | |
>>>8 | D4 D3 D2 D1 |
+ | D(4+3) D(3+2) D(2+1) D1 |
D(4+3) D(3+2) D(2+1) | |
>>>16 | D(4+3) .... |
+ | D(4+3+2+1) |
从上表可以看出,经过4、5两步后,最后的低8位存放的是32位中1的个数。
return i & 0x3f;
最后,返回的结果是取低6位,从源头想,32位中1的个数最多是32位,只需要6位就可以表示。
当然我们也可以对4、5两步进行一些变化,由于4之前,每8位都存储了8位中1的个数,那么我们也可以直接将每个8位中1的个数相加,即:
i = i + (i >>> 8) + (i >>> 16) + (i >>> 24);
最终返回的时候仍然取低6位也是可以的。
二、每3位存储1的个数
结论:3位中1的个数 = 3位的值 - 高2位的值 - 高1位的值
二进制位 | 高2位二进制位 | 高1位2进制位 | 3位中1的个数 |
000 = 0 | 00 = 0 | 0 | 0 - 0 - 0 = 0 |
001 = 1 | 00 = 0 | 0 | 1 - 0 - 0 = 1 |
010 = 2 | 01 = 1 | 0 | 2 - 1 - 0 = 1 |
011 = 3 | 01 = 1 | 0 | 3 - 1 - 0 = 2 |
100 = 4 | 10 = 2 | 1 | 4 - 2 - 1 = 1 |
101 = 5 | 10 = 2 | 1 | 5 - 2 - 1 = 2 |
110 = 6 | 11 = 3 | 1 | 6 - 3 - 1 = 2 |
111 = 7 | 11 = 3 | 1 | 7 - 3 - 1 = 3 |
三、每4位存储1的个数
结论:4位中1的个数 = 4位的值 - 高3位的值 - 高2位的值 - 高1位的值
public int bitCount(int n) {
// 1
n = n - ((n >>> 1) & 0x77777777) - ((n >>> 2) & 0x33333333) - ((n >>> 3) & 0x11111111);
// 2
n = (((n & 0xf0f0f0f0) >>> 4) + (n & 0x0f0f0f0f));
// 3
n = n + (n >>> 8);
// 4
n = n + (n >>> 16);
return n & 63;
}
这段代码的第一步就是实现4位存储1的个数,后续步骤同每2位存储1的个数。
四、每多位存储1的个数
结论:n位中1的个数 = n位的值 - 高(n - 1)位的值 - 高(n - 2)位的值 - ... - 高1位的值
public int bitCount(int n) {
n = n - ((n >>> 1) & 0xdef7bdef) - ((n >>> 2) & 0xce739ce7)
- ((n >>> 3) & 0xc6318c63) - ((n >>> 4) & 0x02108421);
n = ((n + (n >>> 5)) & 0xc1f07c1f);
n = ((n + (n >>> 10) + (n >>> 20) + (n >>> 30)) & 63);
return n;
}
以上这段代码是实现每5位存储1的个数,其中(n >>> 1) & 0xdef7bdef即为每5为中高4位的值,以此类推...
最后将1存储在每个10位中,把4个10位的1添加在一起,即为答案。
此处,也可以采用同每2位存储1的个数同样的思想,把相邻10位的1聚集起来,最后把相邻20位的1聚集起来,最后也可以得到32位中的1。
n = n + (n >>> 10);
n = n + (n >>> 20);
下附力扣题目链接,可自行练习:https://leetcode.cn/problems/number-of-1-bits/description/