剑指Offer对答如流系列 - 二进制中 1 的个数

Posted jefferychenxiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剑指Offer对答如流系列 - 二进制中 1 的个数相关的知识,希望对你有一定的参考价值。

面试题14:二进制中 1 的个数

题目描述

请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如把9表示成二进制是1001,有2位是1。因此如果输入9,该函数输出2。

问题分析与解决

这道面试题归属于 《剑指Offer》位运算章节。遇到二进制相关的问题,很容易想到位运算,虽然种类不多(与、或、异或、左移、右移),但是搞起来是千变万化的。待会再和你侃一些骚操作,我们先看这道题。

(一)思路一

“与运算”有一个性质:通过与对应位上为1,其余位为0的数进行与运算,可以指定整数某一位上的值。

这道题中,先把整数n与1做与运算,判断最低位是否为1;如果是1,则计数器加1;否则,没有计数器的事情。
接着把1左移一位(移位运算总要快于乘除),与n做与运算,可以判断次低位是否为1……反复左移,即可对每一个位置都进行判断,从而可以获得1的个数。这种方法需要循环判断32次。(int类型4个字节,一个字节8位)

这是最容易想到的一种方法。

当然,你可能会觉得为什么不右移整数n?那是因为 对于负数而言,最高位是1,右移的时候会保证负数的性质,即最高位仍是1,源源不断的右移,源源不断的1,是死循环啊。

    public int NumberOf1(int n) {
            int count = 0;
            int flag = 1;
            while (flag != 0) {
                if ((flag & n) != 0) {
                    count++;
                }
                flag = flag << 1;
            }
            return count;
        }

(二)思路二

与运算还有一个性质:把一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0。

动笔画画呗,非常好证明。

循环利用与运算能将1变成0的这条性质,计算出1的个数

    public int NumberOf1(int n) {
            int count = 0;
            while (n != 0) {
                count++;
                n = (n - 1) & n;
            }
            return count;
        }

盘点位运算的骚操作

(1)移位运算(左移or右移)快于乘除

如果只针对2 进行乘除 强烈建议采用这种方法。

(2)一个整数减去1,再和原整数做与运算,会把该整数最右边的1变成0

上面题中有体现
判断一个数是否是2的次幂 也可以用这种方法呀。 n&(n-1)为0 说明这个数就是2的次幂

(3)通过右移判断二进制某位是0还是1

上面思路一用到的是这种思想,不过是用1进行左移实现的,思想保持一致。

(4)判断奇偶数

二进制中,末位是0→偶数,末位是1→奇数。
n&1=0 --- 偶数
n&1=1 --- 奇数
求求你,看完这篇文章后,判断奇偶不要再取余了

(5)两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身

如果给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出这只出现一次的数 。直接一个for循环,所有的值进行异或,结果就是那个只出现一次的数。

(6)两个数的交换

高逼格啊,相信你会在某些框架的源码中看到的

number1 = number1 ^ number2;
number2 = number1 ^ number2;
number1 = number1 ^ number2;

(7)不用判断语句,求整数的绝对值

正负通用 : i^(i>>31) + i>>>3
负数专用 : i^-1 + 1
相信我 这个看框架源码 会遇见的。

       public static void main(String[] args) {
            int i=-11,j=11;
            // 正负通用
            System.out.println(i+"的绝对值是"+((i^(i>>31))+(i>>>31)));
            System.out.println(j+"的绝对值是"+((j^(j>>31))+(j>>>31)));
            //已知i是负数的用法
            System.out.println(i+"的绝对值是"+((i^-1)+1));
        }

技术图片

位运算高阶操作

上面举的用起来已经很欢乐了。要不要见一下一个大厂的专门考察位运算的面试题?

不用 + - * / 实现 加减乘除

(一)加法

对于位运算,还有一些你需要了解的基本性质

(1)二进制位异或运算相当于对应位相加,不考虑进位

1 ^ 1 = 0 ---> 1 + 1 = 0 (当前位值为0,进一位)
1 ^ 0 = 1 ---> 1 + 0 = 1 (当前位值为1)
0 ^ 0 = 0 ---> 0 + 0 = 0 (当前位值为0)

(2)二进制位与运算相当于对应位相加之后的进位

1 & 1 = 1 ---> 1 + 1 = 0 (当前位的值进一位)
1 & 0 = 0 ---> 1 + 0 = 1 (当前位的值不进位)
0 & 0 = 0 ---> 0 + 0 = 0 (当前位的值不进位)

(3)两个数相加:对应二进制位相加的结果 + 进位的结果

3 + 2 --> 0011 + 0010 --> 0011 ^ 0010 + ((0011 & 0010) << 1) ---> (0011 ^ 0010) ^ ((0011 & 0010) << 1)
当进位之后的结果为0时,相加结束

实现如下:

  int add(int a,int b){
        int s;
        int c;
        while(b != 0){
            // 获得求和位
            s = (a^b);
            // 获得进位,然后需要向做移动一位表示进位
            c = ((a&b)<<1);
            a = s;
            b = c;
        }
        return a;
    }

换成递归的写法会更清晰点

    int add(int a,int b){
        // 假如进位为0
        if(b ==0){
            return a;
        }
        // 获得求和位
        int s = a^b;
        // 获得进位,然后需要向做移动一位表示进位
        int c = ((a&b)<<1);
        return add(s,c);
    }

(二)减法

本质上是使用加法来做的,只是加了一个负数嘛

首先我们得先将被减数取反(a取反就是~a+1,补码知道吧)

   // 取得相反数
    int adverse(int a){
        return add(~a,1);
    }
    // 减法函数 其中add就是前面的加法函数
    int subtract(int a,int b){
        return add(a,adverse(b));
    }

(三)乘法

这个本质上也是靠加法来实现的,比如说6 * 100, 不就6个100相加吗?

不过我们要多考虑的事情是 正负的 处理

    // 负数返回-1,正数返回0
    int getsign(int i){
        return (i >> 31);
    }
    // 如果为负数,则进行取反
    int toPositive(int a) {
        if (a >> 31 == -1)
            // 进行取反
            return add(~a, 1);
        else
            return a;
    }

然后就用加法实现呗

int multiply(int a, int b){
        boolean flag = true;
        // 如果相乘为正数,flag为false
        if (getsign(a) == getsign(b))
            flag = false;
        // 将a取正数
        a = toPositive(a);
        b = toPositive(b);
        int re = 0;

        while (b!=0) {
            // 相加
            re = add(re, a);
            // b进行次数减一
            b = subtract(b, 1);
        }
        // 假如结果是负数,则进行取反
        if (flag)
            re = adverse(re);
        return re;
    }

但是 若是单纯靠加法来实现,数字比较大的时候感觉实在太慢了。

考虑我们现实生活中手动求乘积的过程,这种方式同样适用于二进制,下面我以13*14为例,演示如何用手动计算的方式求乘数和被乘数绝对值的乘积。
技术图片

步骤和十进制的类似

(1) 判断乘数是否为0,为0跳转至步骤(4)
(2) 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中;
(3) 被乘数左移一位,乘数右移一位;回到步骤(1)
(4) 确定符号位,输出结果;

实现的话,就这样

int multiply(int a, int b) {
        boolean flag = true;
        // 如果相乘为正数,flag为false
        if (getsign(a) == getsign(b))
            flag = false;
        // 将a取正数
        a = toPositive(a);
        b = toPositive(b);
        int re = 0;
        while (b!=0) {
            // 假如b的最后一位为1  不就是奇偶的判断嘛 上面骚操作提过了 别问 问就是看上面
            if((b&1) == 1){
                // 相加
                re = add(re, a);
            }
            b = (b>>1);
            a = (a<<1);
        }
        // 假如结果是负数
        if (flag)
            re = adverse(re);
        return re;
    }

(四)除法

除法运算很容易想到可以转换成减法运算,就是不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。

这里需要注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。

实现上与乘法第一种思路大同小异,但是效率低。

要想提高效率,可以参考乘法实现的第二种思路就是列竖式。这里就只提供第二种思路咯

 int divide(int a,int b){
        boolean flag = true;
        // 如果相除为正数,flag为false
        if (getsign(a) == getsign(b))
            flag = false;
        // 将a取正数
        a = toPositive(a);
        b = toPositive(b);
        int re = 0;
        int i = 31;
        while(i>=0){
            // 如果够减
            // 不用(b<<i)<a是为了防止溢出
            if((a>>i)>=b){
                // re代表结果
                re = add(re, 1<<i);
                a = subtract(a, (b<<i));
            }
            // i减一
            i = subtract(i, 1);
        }
        // 假如结果是负数
        if (flag)
            re = adverse(re);
        return re;
    }

以上是关于剑指Offer对答如流系列 - 二进制中 1 的个数的主要内容,如果未能解决你的问题,请参考以下文章

剑指Offer对答如流系列 - 从1到n整数中1出现的次数

剑指Offer对答如流系列 - 打印1到最大的n位数

剑指Offer对答如流系列 - 数组中数字出现的次数

剑指Offer对答如流系列 - 圆圈中最后剩下的数字

剑指Offer对答如流系列 - 链表中倒数第k个结点

剑指Offer对答如流系列 - 数字序列中某一位的数字