【剑指Offer】二进制中1的个数
题目描述
输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
补码
解题前,我们先来了解一下补码。在计算机系统中,数值都是用补码来表示和存储的。
而原码就是数值的二进制数表示,最高位1表示负数。
以32位数值举例
1的原码就是
-1的原码就是
正数的补码等于原码
负数的补码等于其原码按位取反后(除了最高位)加1,比如-1的补码就是32个1
使用补码的好处在于,可以将符号位和数值位统一处理,加法与减法也可以统一处理。
比如3 - 1
,等价于3 + (-1)
,则对于计算机来说将3和-1的补码直接相加就可以,计算过程如下图所示。如果直接使用数值的原码表示,则不会得到正确的结果,感兴趣的同学可以计算试一下,这里不再赘述。
解法1
对于本题,首先想到的是将二进制数一直右移,这样的话该数的每一位都会依次成为最低位,然后将该数和1相与,计算1的个数。(由于1只有最低位是1,其他位都是0,某个数和它相与后,结果如果是1,就说明该数最低位是1,否则就是0)
按照以上思想,实现的代码如下,但是请注意,这样的写法是错误。没有考虑负数的情况,和正数右移最高位补0不同的是,负数右移最高位补1,这样就会有无数个1,导致死循环。
public int NumberOf1(int n)
{
int count = 0;
// 没有考虑负数,一直不会等于0
while(n != 0)
{
if ((n & 1) == 1)
count++;
// 负数右移最高位补1
n >>= 1;
}
return count;
}
既然将目标数右移和1与行不通,那么我们可以反过来,将1不断左移(从最低位到最高位每一位依次是1,其他位是0),然后和目标数相与来求1的个数。具体过程如下图所示
实现代码
public int NumberOf1(int n)
{
int unit = 1, count = 0;
while (unit != 0)
{
if ((unit & n) != 0)
count++;
unit <<= 1;
}
return count;
}
解法2
上面解法1的时间复杂度是O(n的位数),n有所少位就要循环多少次。可以利用一个小技巧,降低算法的时间复杂度。
先来看一个例子,对于任意一个数将其减1,比如7
7的补码表示是
(7的补码)
减1后为6,补码表示如下。如果再和7
相与,得到的值仍为6。得到的值相当于把7
从右边数的第一个1被变成了0
(6的补码)
比如6,再减1,为5,补码表示如下
(5的补码)
如果再和6
相与,得到的值为4。补码表示如下,得到的值相当于把6
从右边数的第一个1被变成了0
如果用负数进行测试,也是一样的结果。
由此我们可以发现对于数值n,将n - 1后再和n相与,得到的值相当于将n从右边数的第一个1变成0。n的二进制表示中有多少个1,就能变多少次。实现代码如下,时间复杂度优化为O(n中1的个数)
实现代码
public int NumberOf1(int n)
{
int count = 0;
while (n != 0)
{
count++;
n = (n - 1) & n;
}
return count;
}
更多题目的完整描述,AC代码,以及解题思路请参考这里