题目:
请实现一个函数,输入一个整数,输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入
9,则该函数输出 2。
示例 1:输入:00000000000000000000000000001011 输出:3
解释:输入的二进制串
00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:输入:00000000000000000000000010000000 输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:输入:11111111111111111111111111111101 输出:31
解释:输入的二进制串
11111111111111111111111111111101 中,共有 31 位为 '1'。
这个题目描述很清楚,但是做完了之后,我觉得挺奇怪的,这个题的标记居然是一个 simple ,因为看到 jdk 源码里的 bitCount() 算法,觉得我们能想出来的什么也不算。
一、换成字符之后进行统计
二进制我们肉眼不容易看到,因此统计的时候,更加直观的显示,然后代码层面上,可读性就很好。
Integer 提供了转换十进制数字的方法,我们转换之后就可以对字符串进行操作,同时统计个数。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
String s=Integer.toBinaryString(n);
int ans=0;
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='1'){
ans++;
}
}
return ans;
}
}
不过,这个方法显然运行速度是比较慢的。
二、位运算(渐入佳境哦)
我们知道计算机本身的 数据存储就是二进制,所以直接对数字进行位操作,对于这个题是比较好的选择。
对于位运算符,前提知识我们来复习如下:
位运算符主要针对二进制,它包括了:“与”、“非”、“或”、“异或”。从表面上看似乎有点像逻辑运算符,但逻辑运算符是针对两个关系运算符来进行逻辑运算,而位运算符主要针对两个二进制数的位进行逻辑运算。
下面是一张参考别人的总结(忘了哪里保存的图)
其中逻辑运算符一栏,针对每一位的操作和解释如下:
按位与运算符(&):都为 1 结果为 1,否则 0
按位或运算符(|):其中有一个 1 结果为 1,否则 0
按位非运算符(~):0 1 互换
异或运算符(^):相同为 1 ,不同为 0
几个移位运算符的总结解释如下:
那么,可以想到,当 x 左移一位,就变成了了 2 倍,右移一位就变成了 1/2 。
扩展一下:x << y 相当于 x*2y ;x >> y 相当于 x/(2y)
(因此我们做除法 /2 的时候,经常用 x >>1,同理乘法 *2 可以用 x<<1,效率会非常可观。)
2.1 按位 与
有了位操作符,我们可以想象这道题,为了计算出二进制的表达中有多少 1 ,只需要访问到每一位,并且判断他是 0 还是 1 进行统计就能达到目的。
问题在于如何访问到每一位?
拆开是不现实的,但我们可以利用与 & 操作。
首先 n & 1。得到的结果就能判断 n 的二进制表达 的 最后一位 是否为 1 。
因为 1 的二进制表达是 0000 … 1 很多个 0 以及最后一位的 1。所以 n & 1 的结果就是最后一位的判定结果。
那么只需要在每次 判定完一次之后,将 n 右移一位就可以。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int ans=0;
while(n!=0){
ans += n&1;
n = n>>>1;
}
return ans;
}
}
2.2 巧用 n & (n-1)
我们先来看 n - 1 操作的性质。
n - 1 在二进制的减法里, 会将 n 最 右边的 1 变成 0 ,此 1 右边的 0 都变成 1。左边的各位不变 。
解释一下:
如果二进制表达的最后一位是 1 ,比如 111(十进制的 7)
111 - 1 = 110,这很明显。
如果二进制表达的最后一位不是1,比如 100(十进制的 4)
100 - 1 = 011,借高位的 1 来计算,也没问题。
可以总结出规律,也就是 “ 会将 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。”
这就说明:
每进行一次 - 1 操作,就会改变 当前 最右边的 为 1 的二进制位 ,并且在他、以及他右边的位上的值都和以前的不一样,而在他左边位置的值都仍然一样。
还是再仔细说一下:
比如还是上面 7 的二进制表示: 100 ;
100 -1 之后变成 011 。
最右边的 1 改变了,此 1 右边的0也都改变了(毕竟这是 最右边 的1,说明右边只有 0 了呀)
那此时从本来的 这个 1 到右边所有的位,都和以前原本的不一样了
所以我们就可以进行一下 n & ( n-1 ),就能达到 消去了当前最右边的 1 ,并且在此 1 的右边也都会全部变成 0 。
那么只要在进行 n & ( n - 1 )操作的同时计数,直到没有 1 ,也就达到了结果。
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int ans=0;
while(n!=0){
ans++;
n = n&(n-1);
}
return ans;
}
}
2.3 补充知识点:无符号数
这道题还有一部分比较重要的知识,体现在括号里 n != 0 的判断条件,这是有关 java 本身对于数字的编码知识。
可以看到题目给了提示:
“you need to treat n as an unsigned value”,你需要把输入考虑成一个无符号的值。
需要注意的是:java的数据都是有符号的,比如 4 个字节的 int 类型,为何数据范围是 -2^31 ~ 2^31-1。而不是用满了整个 2^32次方。就是因为最高位是符号位。
而这道题目要求把输入考虑成无符号的值。
那么首先,我们在 while 条件里只能写 n!= 0,而不能写 n>0.
如果输入的是一个负数,比较了 n > 0 之后不满足,压根不会进入循环,所以写成 !=0 才能保证考虑成无符号值 这一要求。
另外,正因为是考虑无符号数,数位移动只能用 >>> 而不能用 >>。
如果使用有符号右移 >> ,符号位一直添加 1,那么 n != 0 的判断条件就会无法终止。
数位向右移动,高位为1(负数),则补1,低位舍弃;高位为0(正数)则补0,保持符号位不变,低位舍弃。
更多关于java的有符号,无符号操作,这有一篇详细的博客:
https://www.cnblogs.com/yongdaimi/p/5945114.html
三、bitCount()
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
return Integer.bitCount(n);
}
}
想不到吧,java提供了 bitCount() 这个方法。
事已至此,相比你也很想知道 java 本身是如何巧妙实现 bitCount 这个功能的。
源码是怎么考虑这个问题的。
源码很短:
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;
}
看了之后,除了懵逼还是懵逼。
所以花了很长时间重新理解了一下:
总结在了下一篇博客: