【剑指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代码,以及解题思路请参考这里

posted @ 2019-06-20 13:47  iwiniwin  阅读(1543)  评论(0编辑  收藏  举报