剑指Offer-11-二进制中1的个数

题目

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

解析

预备知识

世界上有10种人,一种人知道二进制,而另一种人不知道二进制。
在计算机中数都是采用二进制存放的,这与硬件设计电路(比如与门,放大器)等有关,且二进制只有2种状态,简单高效,故计算机采用二进制。
而参与运算的数在计算机一般用其补码形式存放。

补码是原码减1,除了符号位以外全部按位取反得到的结果。
而原码是符号位加上数值本身的二进制形式表达。
正数的原码与补码相同,比如2的原码为10,其实为0…10,不过前向0被我省略了,但在内存中这些0还是占相应的字节。
而-2的原码为1……10,-2的补码为1111…110。它是通过原码减一,然后除了符号位其余全部取反得到。

我们的四则运算其实在计算机底层都是位运算。位运算有:与,或,异或,左移,带符号右移和无符号右移。
与,或,异或,都是很简单运算,想必你们都会,我就不细说了。对于移位运算可以参考这篇博文,虽然说得是Java中标准,但大多数编程语言都大同小异。

思路一

我们按照移位操作对原数进行无符号右移(不明白看我推荐那个博文)操作,然后与1相与判断该位是否是1,从而决定是否计数,这样扫描一遍数的二进制形式,即可得到1的个数。
之所以采用的是特殊的移位操作——无符号右移,是因为这样才能对负数适用。因为负数的最高位即符号位为1,如果采用的普通的右移操作(即带符号右移),高位会在移位后不断填充1,这样会导致死循环。而采用无符号右移,则会补充0,当符号位最后右移消失后,整个数最终为0,则终止循环。

    /**
     * 普通做法,通过移位操作进行
     * @param n
     * @return
     */
    public static int NumberOf1One(int n) {
        int count = 0;
        while(n != 0) {
            if((n & 1) == 1) {
                count++;
            }
            n >>>= 1;
        }
        return count;
    }

思路二

当然也不用搞这个比较难懂的无符号右移,我们可以把数与1做与运算,从而判断该位是1,然后把1左移1位,即得到2。再把数与2做与运算,从而判断倒数第二位是否是1。重复左移,直到进行32次,因为int型为32位。

    /**
     * 左移辅助变量
     * @param n
     * @return
     */
    public static int NumberOf1Three(int n) {
        int count = 0;
        int t = 1;
        for(int i = 1; i <= 32; i++) {
            if((n & t) != 0) {
                count++;
            }
            t <<= 1;
        }
        return count;
    }

思路三

以上二种做法都是按位扫描,复杂度为O(n)。有没有最好的方法呢?当然有。我们首先把原数做减一操作,看2种情况(数不为0的情况下):
1. 当最后一位是1,减去1后,该位置为0,其余位置保持不变。比如7(111) - 1 = 6(110)
2. 当最后一位不是1,假设第m位为该数二进制的最右边的1,减去1后,第m位变为0,但是第m位之后直到末尾全部变了1。比如12(1100)- 1 = 11 (1011)

通过上面发现,对原数做减一操作,都可以使数的最右边的1变为0。如果是第一情况,该数其他位置不变,但是如果是第二情况,则会导致m位之后变为1,那如何使m位之后还是以前的0呢,做与操作。原数与减一后的数做与操作。即可做到m位之前不变,m位之后也与原数相同(即都为0)。
结论为:把一个整数减去1后与原数做与操作,得到的结果为整数的二进制形式的最右边的一个1变为0。
所以如果原数有n个1,我们只需进行n次这样的减一和与操作即可了。Amazing!

     /**
     *  n &= n - 1
     * @param n
     * @return
     */
    public static int NumberOf1Two(int n) {
        int count = 0;
        while(n != 0) {
            count++;
            n &= n - 1;
        }
        return count;
    }

拓展

拓展1

判断一个整数是不是2的整数次幂。
只需判断是否数的二进制形式中是否只有1个1即可。

    /**
     * 判断一个整数是否是2的整数次方
     * @param num
     * @return
     */
    public static boolean isTwoTimes(int num) {
        return (num & (num - 1)) == 0;
    }

拓展2

给定2个整数,判断最少改变第一个数的二进制中位即可与第二个数相同。
异或即可求出不同位置,因为不同位置异或为1,然后再统计1的个数即可。

    public static int countOfChange(int num1, int num2) {
        int res = num1 ^ num2;
        int count = 0;
        while(res != 0) {
            count++;
            res &= res - 1;
        }
        return count;
    }

猜你喜欢

转载自blog.csdn.net/dawn_after_dark/article/details/80706871