我们不再检查数字的每一个位,而是不断把数字最后一个 1 反转,并把答案加一。当数字变成 0 的时候偶,我们就知道它没有 1 的位了,此时返回答案。
注意:这里我们说的是最后一个 1,而不是最后一位 1,这个 1 可能在任何位上。
这里关键的想法是对于任意数字 n ,将 n 和 n - 1 做与运算,会把最后一个 1 的位变成 0 。为什么?考虑 n 和 n - 1 的二进制表示。
巧用 \(n \& (n - 1)\)
\((n - 1)\) 解析: 二进制数字 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。
\(n \& (n - 1)\) 解析: 二进制数字 n 最右边的 1 变成 0 ,其余不变。
图片 1. 将 n 和 n-1 做与运算会将最低位的 1 变成 0
在二进制表示中,数字 n 中最低位的 1 总是对应 n - 1 中的 0 。因此,将 n 和 n - 1 与运算总是能把 n 中最低位的 1 变成 0 ,并保持其他位不变。
比如下面这两对数:
肯定有人又是看的一脸懵逼,我们拿 11 举个例子:(注意最后一位 1 变成 0 的过程)
使用这个小技巧,代码变得非常简单。
Java
public int hammingWeight(int n) { int sum = 0; // 用来存放 1 的个数 while (n != 0) { sum++; n &= (n - 1); } return sum; } Java内置函数:1 的个数 public int hammingWeight(int n) { return Integer.bitCount(n); } 异或相消(异或运算的特性)我们先来看下异或的数学性质(数学里异或的符号是 \(\oplus\)):
交换律:\(p \oplus q = q \oplus p\)
结合律:\(p \oplus (q \oplus r) = (p \oplus q) \oplus r\)
恒等率:\(p \oplus 0 = p\)
归零率:\(p \oplus p = 0\)
异或运算有以下三个性质。
任何数和 0 做异或运算,结果仍然是原来的数,即 \(a \oplus 0=a\)。
任何数和其自身做异或运算,结果是 0,即 \(a \oplus a=0\)。
异或运算满足交换律和结合律,即 \(a \oplus b \oplus a=b \oplus a \oplus a=b \oplus (a \oplus a)=b \oplus0=b\)。
假设数组中有 2m+1 个数,其中有 m 个数各出现两次,一个数出现一次。令 \(a_{1}\)、\(a_{2}\)、\(\ldots\)…、\(a_{m}\) 为出现两次的 m 个数,\(a_{m+1}\) 为出现一次的数。根据性质 3,数组中的全部元素的异或运算结果总是可以写成如下形式:
\[(a_{1} \oplus a_{1}) \oplus (a_{2} \oplus a_{2}) \oplus \cdots \oplus (a_{m} \oplus a_{m}) \oplus a_{m+1}\]
根据性质 2 和性质 1,上式可化简和计算得到如下结果:
\[0 \oplus 0 \oplus \cdots \oplus 0 \oplus a_{m+1}=a_{m+1}\]
因此,数组中的全部元素的异或运算结果即为数组中只出现一次的数字。
下面我们来举个例子吧:
假如我们有 [21,21,26] 三个数,是下面这样:
回想一下,之所以能用“异或”,其实我们是完成了一个 同一位上有 2 个 1 清零 的过程。上面的图看起来可能容易,如果是这样 (下图应为 26^21):
Java
class Solution { public int singleNumber(int[] nums) { int single = 0; for (int num : nums) { single ^= num; } return single; } } 实例 位1的个数编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。
提示:
请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。
进阶:
如果多次调用这个函数,你将如何优化你的算法?
示例 1:
输入:00000000000000000000000000001011 输出:3 解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。示例 2:
输入:00000000000000000000000010000000 输出:1 解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。示例 3:
输入:11111111111111111111111111111101 输出:31 解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。提示:
输入必须是长度为 32 的 二进制串。
算法
这个方法比较直接。我们遍历数字的 32 位。如果某一位是 1 ,将计数器加一。
我们使用 位掩码 来检查数字的第 \(i^{th}\) 位。一开始,掩码 m=1 因为 1 的二进制表示是
\[0000\ 0000\ 0000\ 0000\ 0000\ 0000\ 0000\ 0001\]
0000 0000 0000 0000 0000 0000 0000 0001
显然,任何数字跟掩码 11 进行逻辑与运算,都可以让我们获得这个数字的最低位。检查下一位时,我们将掩码左移一位。
\[0000\ 0000\ 0000\ 0000\ 0000\ 0000\ 0000\ 0010\]
0000 0000 0000 0000 0000 0000 0000 0010
并重复此过程。
Java
public int hammingWeight(int n) { int bits = 0; int mask = 1; for (int i = 0; i < 32; i++) { if ((n & mask) != 0) { bits++; } mask <<= 1; } return bits; }复杂度分析
时间复杂度:\(O(1)\)。运行时间依赖于数字 n 的位数。由于这题中 n 是一个 32 位数,所以运行时间是 \(O(1)\) 的。
空间复杂度:\(O(1)\)。没有使用额外空间。
逐位判断
根据 与运算 定义,设二进制数字 n ,则有:
若 \(n \& 1 = 0\),则 n 二进制 最右一位 为 00 ;
若 \(n \& 1 = 1\),则 n 二进制 最右一位 为 11 。
根据以上特点,考虑以下 循环判断 :
判断 n 最右一位是否为 1 ,根据结果计数。
将 n 右移一位(本题要求把数字 n 看作无符号数,因此使用 无符号右移 操作)。
算法流程:
初始化数量统计变量 res = 0。
循环逐位判断: 当 n = 0 时跳出。
res += n & 1 : 若 \(n \& 1 = 1\) ,则统计数 res 加一。
n >>= 1 : 将二进制数字 n 无符号右移一位( Java 中无符号右移为 ">>>" ) 。