1.问题描述
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
2.解决方法
2.1 level1
看到这个题目第一反应是什么呢?在回想什么是补码,如何把一个整数转换成2进制。这是一个非常通用的想法。
我们最先知道的就是使用对二整除、求余两种运算,能够保证整个功能实现,那么省下的就是要处理补码的问题,补码是针对负数的,负数的补码符号位为1,并且使原码的取反加一。
但它真正的含义是去除符号位的二进制编码是周期的互补的正数的原码。懂了这个含义以后,我们就可以做最基本的算法了。
public int NumberOf1(int n) {
int flag;
if(n>0){
flag=0;
}
else if(n<0){
flag=1;
//周期互补
n=Integer.MAX_VALUE+n+1;
}
else{
return 0;
}
int allcount=0;
int onecount=0;
while(n>0){
allcount++;
if(n%2==1)
onecount++;
n=n/2;
}
return onecount+flag;
}
2.2 level2
但这首先一是没有进行二进制运算,第二是整除和求余的速度太慢,于是我们应该从二进制位数上考虑。只需要判断最高位是0是1就可以了。
public static int NumberOf1(int n){
int count = 0;
//如果不是0的话
while(n!=0){
//如果是负数,说明最高位是1,计数。
if(n<0){
count++;
}
//否则左移
n = n<<1;
}
return count;
}
2.3 level3
但是这样子还是不行的,因为有很多浪费的循环,它有很多0的循环,我们需要把这些0的循环去掉,就是接下来的这种算法,它可以从右往左把所有的1给统计出来。
public static int NumberOf1(int n){
int count = 0;
while ( n != 0 )
{
//从左到右清除0
n &= n - 1;
count++;
}
return count;
}
2.4 level4
这已经是算法的极限速度了,但是其实Java的JDK中有这么一个API供我们使用:
public static int NumberOf1(int n){
return Integer.bitCount(n);
}
一句话就解决问题了,如果是OJ的话,这句话也应该可以通过。
2.5 level5
但我们真的想知道,JDK里的算法到底是怎么实现的,于是我们去看了JDK的源码,源码里的这个函数是这样写的:
public static int NumberOf1(int n){
n = n - ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n + (n >> 4)) & 0x0F0F0F0F;
n = n + (n >> 8);
n = n + (n >> 16);
return n & 0x0000003F;
}
如果经常看过Java源码的人都知道,java里有很多匪夷所思的算法,它们通常是使用一个莫名的十六进制常量参与运算,而且效果奇快,真的不知道这些人,到底是如何找到这种常量的。
那么这个算法的思想是什么呢?归并算法,这是2路归并,两两一组相加,之后四个四个一组相加,接着八个八个,最后就得到各位之和了。
第一行是计算每两位中的 1 的个数 , 并且用该对应的两位来存储这个个数 ,如 : 01101100 -> 01011000 , 即先把前者每两位分段 01 10 11 00 , 分别有 1 1 2 0 个 1, 用两位二进制数表示为 01 01 10 00, 合起来为 01011000。
第二行是计算每四位中的 1 的个数 , 并且用该对应的四位来存储这个个数 .如 : 01101100 经过第一行计算后得 01011000 , 然后把 01011000 每四位分段成 0101 1000 , 段内移位相加 : 前段 01+01 =10 , 后段 10+00=10, 分别用四位二进制数表示为 0010 0010, 合起来为 00100010 。
下面的各行以此类推 , 分别计算每 8 位 ,16 位 ,32 位中的 1 的个数 。
将 0x55555555, 0x33333333, 0x0f0f0f0f 写成二进制数的形式就容易明白了 。
哪还有没有更快的算法呢?当然,我们的脚步从来没有止境。
2.6 level6
HAKMEM算法,这个来自MIT的算法,当然会更上一层,同样的,它也是使用了常量来进行归并,并且还巧妙的使用了一个取余来做,但很显然是它这个取余操作影响了整体的高端。而且为了让负数也能够这样响应,我特地修改了源码。下面是我修改过的算法,HAKMEM算法的详解可以在这看到HAKMEM算法。
public static int NumberOf1(int n){
int flag=0;
if(n<0){
flag=1;
}
int x;
x = (n >> 1) & 033333333333;
n = n - x;
x = (x >> 1) & 033333333333;
n = n - x;
n = (n + (n >> 3)) & 030707070707;
n = n%63;
return n+flag;
}
3.算法分析
3.1 就是论事
就这个问题来讲,我们首先要摆脱常规思维,把思维放置在二进制的层面,这样才能更快的运算。
当然根据我的测试结果,大家可以猜一猜他们谁快谁慢,事实证明,常规思路的算法要比下面位运算要高出一个数量级,而这里面,要数算法3的速度与算法6的速度最快了。
3.2 思维层面
思维层面上,我们还是要多加思考,灵活运用我们之前学过的经典算法,在特定的层面(二进制)下,运用我们的归并法、分治法,动态规划算法等一系列经典算法,才能在当前的基础上更进一步。
4. 小结
这一节中,我们通过对于一个2进制中1的个数进行了相关的探讨,对于位运算也有了一定的认识。接下来我们会进一步的了解相关知识。