位运算的骚操作
位运算的优势
位运算的速度非常快,仅次于赋值操作,常数时间极好
判断一个正数是否是 2 的幂
题目链接
https://leetcode.cn/problems/power-of-two/
思路分析
如果一个数是 2 的幂,则这个数必然大于 0
对于任意一个 2 的幂,在其二进制中只有一个 1,其余都是 0
利用 Brian Kernighan 算法提取出最右侧的 1,此时这个数的状态就是最接近 n 的 2 的幂的数,如果二者相等,说明该数是 2 的幂
代码实现
java
class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && n == (n & -n);
}
}判断一个整数是不是 3 的幂
题目链接
https://leetcode.cn/problems/power-of-three/
思路分析
如果一个数字是 3 的某次幂,那么这个数一定只含有 3 这个质数因子
1162261467 是 int 型范围内,最大的 3 的幂,它是 3 的 19 次方
这个 1162261467 只含有 3 这个质数因子,如果 n 也是只含有 3 这个质数因子,那么
1162261467 % n == 0
反之如果 1162261467 % n != 0 说明 n 一定含有其他因子
代码实现
java
class Solution {
public boolean isPowerOfThree(int n) {
return n > 0 && 1162261467 % n == 0;
}
}返回 >= n 最小 2 的幂
java
// 已知 n 是非负数
// 返回 >= n 最小 2 的幂
// 如果 int 范围内不存在这样的数,返回整数最小值
public class Near2power {
public static final int near2power(int n) {
if (n <= 0) {
return 1;
}
// 减 1 的目的是为了能让本身就是 2 的幂的数最终返回自己
n--;
// 这里的目的是让二进制中最左侧的 1 以及后面的位都变成 1
// int 是 32 位,每一次 | 运算都会以前一个状态作为基础
// 为了将二进制中最左侧的 1 以及后面的位都变成 1
// 每次 | 运算都会处理 2 的幂次方个 1(画图就知道了)
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 在前面的基础上加一,得到的数就是 2 的幂
return n + 1;
}
public static void main(String[] args) {
int number = 100;
System.out.println(near2power(number));
}
}求区间所有数字 & 的结果
题目链接
https://leetcode.cn/problems/bitwise-and-of-numbers-range/
思路分析
(1)明确 & 运算的性质
& 运算的性质:同为 1 结果为 1,否则为 0,即只要有 0 存在 & 的结果为 0
若两个数相等,或者说和自己 & 运算,则两个数 & 运算的结果为本身
(2)本题思路
一堆数字 & 运算,如果二进制位中出现 0, 该二进制位 & 运算的结果为 0,我们只需要关注二进制位为 1 的,根据二进制转十进制的方式理解,因为最后 & 运算的结果由这些二进制位为 1 的所影响,二进制为 0 的最后也不会累加到结果中,也就是说我们需要关注哪些位的 1 可以被保留下来,而这就是该区间中所有数 & 运算的结果
left 到 right 这个区间的是连续的,每次让 right 减一,二进制位中也会减去 1,得到的结果记为 a,则 a 必定在 left 到 right 这个区间中,而这个状态的存在必定会导致 right(即减一之前的数) 的二进制位中最右侧的 1 无法留下(同为 1, & 运算的结果为 1,而减一必然导致两个数的同一个二进制位不同,& 的结果必然为 0),right 继续减小
若每次减一后都发现 right > left 则必然是上面这种情况,继续减一
一直减到 right <= left,循环退出,此时两数相等,根据 & 运算的性质,二者 & 运算的结果为本身,二进制位中前面的 1 肯定能留下,这些二进制位对最后的记过是会产生影响的,因为要求是 left 到 right 区间,所以 < left 的数的二进制位对最后的结果是没有影响的,则此时的二进制状态表示的十进制数就是最后在该区间所有数 & 运算的结果

代码实现
java
class Solution {
public int rangeBitwiseAnd(int left, int right) {
while (left < right) {
// 每次减去最右侧的 1
right -= right & -right;
}
// 第一种情况:left = right,& 运算的结果为本身,所有的 1 都会保留
// 第二种情况:left < right,& 运算的结果为 0,对结果不会产生影响
// 可以忽略,right 继续减小
return right;
}
}复杂度分析
时间复杂度:O(1)
位运算的速度非常快,仅次于赋值操作,常数时间极好,right 的二进制位中有几个 1,while 就循环几次,对于 int 数值来说,最多 32 次结束
逆序一个二进制状态
题目链接
https://leetcode.cn/problems/reverse-bits/
交换过程
以 8 位为例
a b c d e f g h --> h g f e d c b a
第一步:1 v 1 交换(1 个为一组)
b a d c f e h g
第二步:2 v 2 交换(2 个为一组)
d c b a h g f e --> dc ba hg fe
第三步:4 v 4 交换(4 个为一组)
h g f e d c b a --> hgfe dcba位运算思路
按照前面提到的分组思想,实现交换
先去 & 一个数,保留分组的位数
位移实现错位,用 | 运算实现交换

代码实现
java
class Solution {
public int reverseBits(int n) {
// 例子:a b c d e f g h
// & 运算的目的是为了获取不同分组的数,为交换做准备
// 位移的目的是为了实现错位,最后通过 | 运算实现交换
// 1 个二进制位为一组,a:1010,5:0101
n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);
// 2 个二进制位为一组,c:1100,3:0011
n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);
// 4 个二进制位为一组,f:1111,0:0000
n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);
// 8 个二进制位为一组,ff:8 个 1,00:8 个 0
n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);
n = (n >>> 16) | (n << 16);
return n;
}
}求二进制中有几个 1
题目链接
https://leetcode.cn/problems/hamming-distance/
思路分析

以长度为 8 位的二进制数字为例,理解从 1 的计数转为 2 的计数过程中各个运算的作用
第一步:& 一个数字,目的是提取出计数为 2 中的数,比如 0101,这一次 & 运算就会提取出第 2 个 1 和第 4 个 1(先提取后面的)
第二步:位移,再 & 同一个数,位移是为了实现错位,再次 & 同一个数是提取以 2 计数中的另一个数,比如 0101,这一次就是提取两个 0
第三步:将前两次操作的结果相加,即以 2 为计数,两次操作分别计算了这两位中 1 的个数,累加就是以 2 为计数,这两个数中有多少个 1 的结果
代码实现
java
class Solution {
public int hammingDistance(int x, int y) {
// 位不同,异或结果为 1
return cntOnes(x ^ y);
}
// 返回 n 的二进制中有几个 1
public int cntOnes(int n) {
// 5:0101 --> 1 的计数
n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);
// 3:0011 --> 2 的计数
n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
// 0:0000,f:1111 --> 4 的计数
n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);
// 00:8 个 0,ff:8 个 1 --> 8 的计数
n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);
// 0000:16 个 0,ffff:16 个 1 --> 16 的计数
n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff);
return n;
}
}