LeetCode初级算法之其他:191.位1的个数
题目信息
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。
提示:
- 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
- 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。
示例 1:
1 | 输入:00000000000000000000000000001011 |
示例 2:
1 | 输入:00000000000000000000000010000000 |
示例 3:
1 | 输入:11111111111111111111111111111101 |
提示:
- 输入必须是长度为
32
的 二进制串 。
进阶:
- 如果多次调用这个函数,你将如何优化你的算法?
题解一:辗转相除
首先理清主逻辑,判断一组个其中有多少个1,对二进制来说每个位只有0或者1,对我们熟悉的十进制来说有0,1,2 ~,9。如果是十进制怎么判断这个位是非0呢,立马就知道是除以10
1 | 110 (十进制) |
也就是说我们一步一步去判断每个位是否是0即可,对二进制来说非0也就是1,那么1的计数加一。
1 | public int hammingWeight(int n) { |
那么这里有个问题就是对于负数是错误的,因为负数的二进制以补码呈现,像上述测试用例是-3,按照我们的上述逻辑应该是两个一和正3一样,就算加上最高位符号位的负号1也只能是三个一,但实际上负数以补码呈现的就像上述结果实际上是31个一。
因此上述解只是在数字层面的二进制是没问题的,在代码里面因为一些编译的问题就对负数就不行了
题解二:位运算
既然上述解是这样的问题,那我们就不去在数字层面弄,就按照它的规则去数一的个数,也不用转换什么的。其实是和解法一差不多的。因为把解法一翻译一下会变成如下:
1 | public int hammingWeight(int n) { |
翻译就是把除运算换成位移运算,除以一个进制位就等于向右位移一次,对个位取余就等于与1进行与运算。这样逻辑上虽然和上面一样但是对于负数就不一样了
在代码中对于正数来说除以2和向右位移一位是一样的,负数就不一样了因为是一个补码串,你位移它就是用这个串去位移数的就是这个串,但你除以2得到的结果是和它的绝对值除2是一样的然后加个负号。如下:
1 | 3(8位) |
但上面的代码是有问题的,上面的循环是为了循环到最高位的非0数,遍历完毕,这样不用遍历整个32位
对于负数来说就有问题,负数位移永远没有尽头,所以得改成for循环就循环这32位即可
1 | public int hammingWeight(int n) { |
时间与空间复杂度都是O(1)
题解三:位运算优化
前面题解一不用遍历完32为比如00010100
只用遍历到最高位的1即可也就是遍历5个。到了题解二位运算不能去判断是因为负数的二进制一直往右位移也永远不会等于0会进入无限循环。
所以只能找另外的方式怎么一步一步的计数1的出现然后在数字里面去掉或者变为0,直到最后一个1处理完,整个数就是0了就没必要继续遍历了。这个方法就是n & (n-1)
,可以演示一下:
1 | 数字7也就是 0111 |
也就是所每次都会去掉一个一,这个时候就可以计数一下。直到最终是0了,也就统计完最后一个1了。
1 | public int hammingWeight(int n) { |
空间上去了一点可能是少了for循环的i
总结
这一题的话,主要是对于负数在Java里它的一个计算结果和常规是不一样的(这里不展开),因此就用位运算也就不存在转换。第二点就是对于一个int类型的整数它是32位,那么判断32位里有多少个1时,要不要遍历完32位才能出结果?这里就会有两个迭代方式一个是每次位移一个也就是丢掉最右边的数直到数字为0了后面也就不要遍历了,另一个是每次将最低位的1变为0,后者是更优的。因为001011
对于第一种要遍历4次也就是每次丢一个直到最终数字为0,第二种方式只需要3次n & (n-1)
能直接跳到最低位的1处理成0,中间的0都是直接跳过。n & (n-1)
这个运算可以留意一下。还有一种最差的解法也就是转二进制串再数个数这样就没什么意义了相当与遍历了两次Integer.toBinaryString(n)
。