剑指Offer 面试题15 二进制中 1 的个数(三种方法:字符转换,位运算,以及bitCount方法源码分析)

题目:

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

示例 1:输入:00000000000000000000000000001011 输出:3 
解释:输入的二进制串
00000000000000000000000000001011 中,共有三位为 '1'。

示例 2:输入:00000000000000000000000010000000 输出:1 
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
 
示例 3:输入:11111111111111111111111111111101 输出:31 
解释:输入的二进制串
11111111111111111111111111111101 中,共有 31 位为 '1'。

这个题目描述很清楚,但是做完了之后,我觉得挺奇怪的,这个题的标记居然是一个 simple ,因为看到 jdk 源码里的 bitCount() 算法,觉得我们能想出来的什么也不算。

一、换成字符之后进行统计

二进制我们肉眼不容易看到,因此统计的时候,更加直观的显示,然后代码层面上,可读性就很好。

Integer 提供了转换十进制数字的方法,我们转换之后就可以对字符串进行操作,同时统计个数。

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        String s=Integer.toBinaryString(n);
        int ans=0;
        for(int i=0;i<s.length();i++){
            if(s.charAt(i)=='1'){
                ans++;
            }
        }
        return ans;
    }
}

不过,这个方法显然运行速度是比较慢的。

二、位运算(渐入佳境哦)

我们知道计算机本身的 数据存储就是二进制,所以直接对数字进行位操作,对于这个题是比较好的选择。

对于位运算符,前提知识我们来复习如下:

位运算符主要针对二进制,它包括了:“与”、“非”、“或”、“异或”。从表面上看似乎有点像逻辑运算符,但逻辑运算符是针对两个关系运算符来进行逻辑运算,而位运算符主要针对两个二进制数的位进行逻辑运算。

下面是一张参考别人的总结(忘了哪里保存的图)

在这里插入图片描述
其中逻辑运算符一栏,针对每一位的操作和解释如下:

按位与运算符(&):都为 1 结果为 1,否则 0
按位或运算符(|):其中有一个 1 结果为 1,否则 0
按位非运算符(~):0 1 互换
异或运算符(^):相同为 1 ,不同为 0

几个移位运算符的总结解释如下:
在这里插入图片描述

那么,可以想到,当 x 左移一位,就变成了了 2 倍,右移一位就变成了 1/2 。

扩展一下:x << y 相当于 x*2y ;x >> y 相当于 x/(2y)
(因此我们做除法 /2 的时候,经常用 x >>1,同理乘法 *2 可以用 x<<1,效率会非常可观。)

2.1 按位 与

有了位操作符,我们可以想象这道题,为了计算出二进制的表达中有多少 1 ,只需要访问到每一位,并且判断他是 0 还是 1 进行统计就能达到目的。

问题在于如何访问到每一位?

拆开是不现实的,但我们可以利用与 & 操作。
首先 n & 1。得到的结果就能判断 n 的二进制表达 的 最后一位 是否为 1 。

因为 1 的二进制表达是 0000 … 1 很多个 0 以及最后一位的 1。所以 n & 1 的结果就是最后一位的判定结果。

那么只需要在每次 判定完一次之后,将 n 右移一位就可以。

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int ans=0;
        while(n!=0){
            ans += n&1;
            n = n>>>1;
        }
        return ans;
    }
}

2.2 巧用 n & (n-1)

我们先来看 n - 1 操作的性质。

n - 1 在二进制的减法里, 会将 n 右边的 1 变成 0 ,此 1 右边的 0 都变成 1。左边的各位不变

解释一下:
如果二进制表达的最后一位是 1 ,比如 111(十进制的 7)
111 - 1 = 110,这很明显。
如果二进制表达的最后一位不是1,比如 100(十进制的 4)
100 - 1 = 011,借高位的 1 来计算,也没问题。

可以总结出规律,也就是 “ 会将 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。”

这就说明:

每进行一次 - 1 操作,就会改变 当前 最右边的 为 1 的二进制位 ,并且在他、以及他右边的位上的值都和以前的不一样,而在他左边位置的值都仍然一样。

还是再仔细说一下:
比如还是上面 7 的二进制表示: 100 ;
100 -1 之后变成 011 。
最右边的 1 改变了,此 1 右边的0也都改变了(毕竟这是 最右边 的1,说明右边只有 0 了呀)
那此时从本来的 这个 1 到右边所有的位,都和以前原本的不一样了

所以我们就可以进行一下 n & ( n-1 ),就能达到 消去了当前最右边的 1 ,并且在此 1 的右边也都会全部变成 0 。

那么只要在进行 n & ( n - 1 )操作的同时计数,直到没有 1 ,也就达到了结果。

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int ans=0;
        while(n!=0){
            ans++;
            n = n&(n-1);
        }
        return ans;
    }
}

2.3 补充知识点:无符号数

这道题还有一部分比较重要的知识,体现在括号里 n != 0 的判断条件,这是有关 java 本身对于数字的编码知识。

可以看到题目给了提示:

“you need to treat n as an unsigned value”,你需要把输入考虑成一个无符号的值。

需要注意的是:java的数据都是有符号的,比如 4 个字节的 int 类型,为何数据范围是 -2^31 ~ 2^31-1。而不是用满了整个 2^32次方。就是因为最高位是符号位。

而这道题目要求把输入考虑成无符号的值。

那么首先,我们在 while 条件里只能写 n!= 0,而不能写 n>0.
如果输入的是一个负数,比较了 n > 0 之后不满足,压根不会进入循环,所以写成 !=0 才能保证考虑成无符号值 这一要求。

另外,正因为是考虑无符号数,数位移动只能用 >>> 而不能用 >>。

如果使用有符号右移 >> ,符号位一直添加 1,那么 n != 0 的判断条件就会无法终止。

数位向右移动,高位为1(负数),则补1,低位舍弃;高位为0(正数)则补0,保持符号位不变,低位舍弃。

更多关于java的有符号,无符号操作,这有一篇详细的博客:
https://www.cnblogs.com/yongdaimi/p/5945114.html

三、bitCount()

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        return Integer.bitCount(n);
    }
}

想不到吧,java提供了 bitCount() 这个方法。

事已至此,相比你也很想知道 java 本身是如何巧妙实现 bitCount 这个功能的。
源码是怎么考虑这个问题的。

源码很短:

public static int bitCount(int i) {
     // HD, Figure 5-2
     i = i - ((i >>> 1) & 0x55555555);
     i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
     i = (i + (i >>> 4)) & 0x0f0f0f0f;
     i = i + (i >>> 8);
     i = i + (i >>> 16);
     return i & 0x3f;
}

看了之后,除了懵逼还是懵逼。

所以花了很长时间重新理解了一下:

总结在了下一篇博客:

java 的 bitCount()源码超详解

猜你喜欢

转载自blog.csdn.net/weixin_42092787/article/details/106606857
今日推荐