231. 2 的幂
-
1. 除法 , 不断循环判断, 如果能被 2 整除,就不断除以 2 ,直到不能被 2 整除为止,最后结果如果是 1 ,说明可以除尽,是 2 的幂次方,否则就不是。
-
特判: 负数 和 0 不可能由 2 的 x 次方得到 ,直接返回 false 。
如果一个数是 2 的幂,不断除以 2 最终会变成 1:16/2=8, 8/2=4, 4/2=2, 2/2=1
-
2. 判断数字的二进制中是否只含1个1 ,因为如果一个数是 2 的幂,那么该数的二进制中只有 1个1 。
-
判断二进制中是否只含 1个1 有两种方法:
-
1)利用 n & (n - 1) 去掉 n 的最后一个 1 ,判断结果是否是 0 即可 ,如果是 0 说明只含 1个1
-
2)利用 n & -n 获取到 只含最后一位1 的数,判断结果 跟原数字是否相等 即可 ,如果跟原数字相等,说明 只含 1个1
可以发现整型范围内如果是 2 的幂次,那么二进制中一定只有一个 1,所以只需判断二进制中是否只包含一个 1 即可,有两种方法:
- n & (n - 1) == 0 :去掉最后一个 1,看是否为 0
- n & -n == n :只保留最后一位 1,看是否跟原数相同
n & (n - 1) 和 n & -n 是两个常用操作,对于这两个操作的解释如下:
下面举例说明一下,首先看 n & (n - 1) 的操作:
- 如果是 2 的幂,例如 2^4 :
- 可以发现如果一个数是 2 的幂,执行完 n & (n - 1) 的结果会是 0
- 如果不是 2 的幂,例如 14:
- 可以发现如果一个数不是 2 的幂,执行完 n & (n - 1) 的结果不是 0 ,只是将最后一个二进制位上的 1 变为了 0
接着看 n & -n 的操作:
- 如果是 2 的幂,例如 2^4 :
- 可以发现如果一个数是 2 的幂,执行完 n & -n 的结果和原来的数自身相等
- 如果不是 2 的幂,例如 248:
- 可以发现如果一个数不是 2 的幂,执行完 n & -n 的结果不会和原来的数字相等,只会保留所有二进制位的 1 中最后一个 1 ,而其他的 1 都置为了 0
-
3. 判断是否为最大 2 的幂的约数 ,在整型范围内,最大的 2 的幂为 2^30 , 记为 BIG , 我们只需判断 n 是否是 BIG 的约数即可。(即判断 BIG 是否是 n 的整数倍 BIG % n 是否等于 0 )
-
注意点: 2^30 可以直接写成 1 << 30 。因为最大的整数是 2^31 - 1 ,所以可知道最大的 2 的幂是 2^30 。
326. 3 的幂
-
1. 除法 ,不断循环判断,如果是 3 的倍数,就 除以 3 ,直到不能被 3 整除为止,最后结果如果是 1 ,说明可以除尽,是 3 的幂次方 ,否则就不是。
-
特判: 负数 和 0 不可能由 3 的 x 次方得到,直接返回 false 。
如果一个数是 3 的幂,不断除以 3 最终会变成 1:81/3=27, 27/3=9, 9/3=3, 3/3=1
-
2. 判断是否为最大 3 的幂的约数 ,在整型范围内,最大的 3 的幂为 3^19 = 1162261467 ,记为 BIG , 我们只需判断 n 是否是 BIG 的约数即可。(即判断 BIG 是否是 n 的整数倍 BIG % n 是否等于 0 )
-
注意点:求 3^19 ,可以 利用50. Pow(x, n)代码快速幂求解,也可以直接 Math.pow(3, 19) 。
这个题相比于前面一道而言,无法使用位运算技巧来解决,对于方法 2 的解法,困难点在于不借助计算器的情况下,我们很难知道整型范围内最大的 3 的幂次的值是多少,如果通过代码计算则肯定会用到循环或者递归,而题目又不让你用循环或者递归。。 所以这道题又是脑筋急转弯的题,意义不大。
191. 位1的个数
-
1. 利用 n & (n - 1) 每次去掉最后一位的 1 , 只要 n != 0 就一直执行这个操作,每执行 1 次就 计数+1 。最终 n 会变成 0 退出循环。
-
2. 判断 n & 1 的值,如果结果是 1 就计数 累加 ,然后 将 n 每次 右移 1 位, 其实是在不断检查n的最低位是否为1。循环条件: n 总共右移 32 次结束,也可以使用 n != 0 。
-
3. 将 1 每次左移 i 位 ( i 从 [0,31] ),然后和 n 相与,看结果是否是 0 , 不是 0 就 计数 。
方法 1 的代码:
该方法的时间复杂度:O(logn)
方法 2 的代码:
注意,这里如果使用 n != 0 作为循环条件,n 要使用无符号右移,否则会出现循环。
方法 3 的代码:
338. 比特位计数
-
1. 位运算 ,遍历 [0, n] 中的每个 i , 调用191.题代码统计1的个数即可。
这个时间复杂度是 O(nlogn)
-
2. 动态规划 + 最高有效位 ,创建长度 n+1 的 dp 数组,令 dp[i] 表示数字 i 的「 位1比特数 」 ,初始 dp[0] = 0 , 遍历[1, n],求解剩余每个 dp 值,最终返回 dp 数组。
-
对于遍历遇到的每个正整数 x , 如果可以知道 ≤ x 的最大的正整数 y ,且 y 是 2 的整数次幂,则 y 的二进制只有最高位是 1 ,其余都是 0 ,此时称 y 为 x 的「 最高有效位 」。
-
令 z = x − y ,显然 0 ≤ z < x 且 x 比 z 二进制只多 1 个 1 ,则 dp[x] = dp[z] + 1 ,因此只需找到这样的一个 y 它是 2 的整数次幂即可,
-
如何判断一个正数二进制上是否只有最高位是 1 :可以利用公式 n & (n - 1) 去掉最后一位的 1 ,然后看结果是否为 0 ,如果为 0 ,则该数就是要找的 y 。(也就是2的幂)
-
由于是从小到大遍历,因此遍历到 x 时 dp[x − y] 的值已知,则 dp[x] = dp[x − y] + 1 。
注意:这里的y要定义在for循环的外面,当x非2的幂时,y其实是上次计算的值,它距离本次的x,是离它最近的2的幂。当x是2的幂时,y等于x,dp[x] = dp[0] + 1 = 1,而2的幂也确实只有 1 个 1。
这个过程其实就是每次利用了距离 x 最近的 2 的幂来计算当前的 dp 值 ,下表展示了 [1 - 10] 中的每个 x 的 dp 值计算过程,可以参考理解:
从表中可以看出 dp 计算时等号右边的 dp 值一定是在前面计算过了的,另外,当前计算出的 y 的值,可能对下一个枚举的 x 值有效,因此这就是为什么 y 要定义在 for 循环的外面,我们要记住它。
-
3. 动态规划 + 最低有效位 , 创建长度 n+1 的 dp 数组,令 dp[i] 表示数字 i 的「 位1比特数 」 , 初始 dp[0] = 0 , 遍历[ 1, n] ,求解剩余每个 dp 值,最终返回 dp 数组。
-
对于 遍历遇到的每个正整数 x , x/2 相当于 x 右移 1 位, 对于 偶数 ,二进制最低位是 0 ,因此右移一位后 1 的个数不变, 对于 奇数 ,二进制最低位是 1 ,因此右移一位后会少 1 个 1 。
-
因此如果 dp[x/2] 的值已知,则可以得到 dp[x] 的值: 如果 x 是 偶数 ,则 dp[x] = dp[x/2] ,如果 x 是 奇数 ,则 dp[x] = dp[x/2] + 1 ,
-
上述两种情况可以合并写成: dp[x] = dp[x/2] + (x & 1) , 因为是从小到大遍历,所以遍历到 x 时 dp[x/2] 的值肯定已知。
下表展示了 [1