这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。
这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐.
左程云的个人空间-左程云个人主页-哔哩哔哩视频 (bilibili.com)

1. 题目一:判断一个整数是不是 2 的幂
 
1.1 2 的幂的定义是什么?
 
只有在二进制下, 任何位上只有一个 1, 才能是 2 的幂, 比如 0000 0001, 0001 0000, 都是 2 的幂.
要让两个2的幂相加后仍然等于2的幂,需要满足以下条件:
-  两个数必须是2的幂:即它们的形式必须是 (2^n) 和 (2^m),其中 (n) 和 (m) 是非负整数。 
-  两个数的指数必须相同:即 (n = m)。 
但是二进制下, 一个“二进制”位置上只能有一个是 1, 所以所有“二进制位”上只有一个 1, 才能是 2 的幂,
1.1.1 逻辑实现
“题目”的解法是:只要将最右侧的 1 提取出来, 判断是不是与本身相等就行
1.1.2 举例说明
0000 0001 这个是 2 的 0 次方 == 1; 0000 0011 这个是 2 的 0 次方加上 2 的 1 次方 == 3; 将最右侧的 1 提取出来是:0000 0001, 1 != 3; 所以 3 不是 2 的幂;
1.1.3 Brian Kernighan 算法
作用:提取出一个数字的二进制位置下的最右侧的“1”.
n & -n  // 最后可以将n这个数字的最右侧的1提取出来.
1.1.4 代码实例
// 需要会 Brian Kernighan 算法  
// 提取出二进制里最右侧的1;  
// 判断一个整数是不是2的幂  
class Solution {  
    public boolean isPowerOfTwo(int n) {  
        return n > 0 && n == (n & -n);  
    }  
}
2. 题目二:判断一个整数是不是 3 的幂
 
如果一个数字是3的某次幂,那么这个数一定只含有3这个质数因子
 1162261467是int型范围内,最大的3的幂,它是3的19次方
 这个1162261467只含有3这个质数因子,如果n也是只含有3这个质数因子,那么
 1162261467 % n == 0
 反之如果1162261467 % n != 0 说明n一定含有其他因子
public static boolean isPowerOfThree(int n) {  
    return n > 0 && 1162261467 % n == 0;  
}
3. 题目三:>= n 最小的 2 的幂
 
3.1 题目描述
已知n是非负数
 返回大于等于n的最小的2某次方
 如果int范围内不存在这样的数,返回整数最小值
比如说:输入的 n 是 13, 那最后返回的结果是:16, 若是输入的 n 是 4, 返回的结果是 4.
 若是 n <= 0, 就直接返回 1 (2 的零次方).
3.2 解法
因为这个直接涉及到了位运算, 直接结合代码进行解释.(下面的代码可以直接复制之后直接运行)
代码实现的意义:将一个数字 n, 然后将 n - 1 这个数字的最左侧的二进制位置的 1 之后的二进制位置全部修改为 1, 然后 + 1, 这样能返回一个 2 的幂.
举一个例子:
假设:n == 45714
00000000 00000000 10110010 10010010 这个数字是:45714, 十进制表示.
先将n--;
00000000 00000000 10110010 10010001 这个数字是:45713, 十进制表示.
n >>> 1 的结果是:
00000000 00000000 01  0110010 1001000 不用管这个数字的结果是多少.不关心十进制表示.
将 n |= n >>> 1 的结果是:
00000000 00000000 11  110010 10010001 我们不关心之后的二进制位置上的数字, 因为之后的数字后续都会变成1, 对这个来说没有什么意义. 此时我们将最左侧的一个 1 右边的一个二进制位置的数字修改为了 1 , 此时最左侧和最左侧右边的一个位置的数字变成了 1 ,然后我们继续实现.
n >>> 2 的结果是:
00000000 00000000 00  111100 10100100
n |= n >>> 2 的结果是:(因为我们上一步已经将两个二进制位置的数字修改为1了, 所以此时我们移动两个位置.)
00000000 00000000 1111  0010 10010001  我们还是不关心后续二进制位置上的数字, 和原来的原因一样.
n >>> 4 的结果是:
00000000 00000000 0000  1111 00101001
将 n |= n >>> 4 的结果是:
00000000 00000000 11111111   10010001
n >>> 8 的结果是:
00000000 00000000 00000000 11111111
n |= n >>> 8 的结果是:
00000000 00000000 11111111 11111111
这样最后的n |= n >>> 16 就没有必要了, 因为:n |= n >>> 16实现之后还是原来的结果. 
00000000 00000000 11111111 11111111 
最后将这个数字 + 1.
00000000 00000001 00000000 00000000 这个就是最后的结果.这个肯定是一个2 的幂, 因为在32个二进制位置中, 只有一个1, 前面的题目也说过这个问题了.
此时先进行n--的意义也应该是有了深入的理解, 因为我们希望输入的数字本身就是2的幂的情况下, 返回这个输入的数字本身, 而不是一个更大的数字.
public class Code03_Near2power {  
  
    public static final int near2power(int n) {  
       if (n <= 0) {  
          return 1;  // 若是n <= 0, 就直接返回1 (2 的零次方).
       }  
       n--;     // 先将n--, 目的是为了能让本身就是2的幂的数最终返回自己, 比如输入4, 返回的值还是4.
       n |= n >>> 1;  
       n |= n >>> 2;  
       n |= n >>> 4;  
       n |= n >>> 8;  
       n |= n >>> 16;  
       return n + 1;  
    }  
  
    public static void main(String[] args) {  
       int number = 13;  
       System.out.println(near2power(number));  
    }  
  
}
4. 题目四:范围内所有数字 & 的结果
 
4.1 题目描述

4.2 题目解法(位运算解法)
这个暴力方法肯定是谁都会, 就不写了, 而且这个暴力方法本来也没有任何意义.
使用位运算的解法:直接结合代码进行讲解了.
注意:二进制的加减法和十进制的加减法是一一对应的, 没有任何区别. 比如:
二进制:            十进制:
01010011           19999
01000000 -         09000 -
--------           -----
00010011           10999
我们此时假设有一个数字, 此时这个数字是 right
0101001101  right 
假设此时 right == left, 那这个数字就可以直接返回了, 因为只有一个数字, 没办法 & 运算.
假设此时 right > left, 那这个数字最右侧的 1 肯定是留不下了(将来肯定会变成 0 ),
因为将 right - 1, 这个数字肯定是在(left ~ right)这个范围的.
0101001100  right - 1, 将 right 和 right - 1 做 & 运算, 最右边的一个二进制位置的结果肯定是 0 .
利用 Brian Kernighan 算法, 将 right - right & (-right), 此时保留结果.
0101001100  这个是结果. 前面的 1 都能留下来.判断一下和 left的关系, 要是 > left 继续往下走, 要是 <= left 就停止.
此时假设 right 还是 > left, 那前面结果最右侧的 1 肯定是留不下了(将来肯定会变成 0 ),
还是按照上面的方式将此时的结果 - 1
0101001011 这个是 上面的结果 - 1 的值. 这个值肯定在(left ~ right)范围内, 和上面的数字做 & 运算,最后的结果肯定是将后面两个 1 消除掉了.
所以此时将 right - right & (-right), 还是使用了 Brian Kernighan 算法, 
0101001000 这个是结果.
总结:步骤就是在 right > left 的情况下, 将 right 在二进制状态下最右侧的二进制的 1 减掉, 此时继续判断 right 和 left 的关系, 要是 right > left, 就继续将 right 在二进制状态下最右侧的二进制的 1 减掉, 要是 right <= left, 就直接停止, 返回 right. 然后继续判断 right 和 left
 的关系, 其中使用到了 Brian Kernighan 算法 和 二进制的加减. 都在上面有说明.
public static int rangeBitwiseAnd(int left, int right) {  
    while (left < right) {  
       right -= right & -right;  
    }  
    return right;  
}
注意:题目 5, 6 都是大牛的实现, 所以我们只需要了解一下, 然后记住, 当成一个模板使用就行了
5. 题目五:逆序二进制的状态
5.1 题目描述

5.2 解法
当然可以直接用 for循环和数组, 然后一个一个地将所有二进制位置进行记录, 最后利用 | 运算返回逆序之后的数字.
5.2.1 暴力解法
这个的实现效率很慢, 所以知道就行了, 不用记住.
public static int reverseBit(int n) {  
    int[] cnts = new int[32];  
  
    for (int i = 0; i < 32; i++) {  
       cnts[i] = (n & (1 << i)) != 0 ? 1 : 0;  
    }  
  
    int ans = 0;  
    for (int i = 31; i >= 0; i--) {  
       ans |= cnts[31 - i] == 1 ? 1 << i : 0;  
    }  
    return ans;  
}
5.2.2 代码实例
看一下大牛的实现:这个是需要记住的. 将来直接使用就行了
public static int reverseBits(int n) {  
    n = ((n & 0xaaaaaaaa) >>> 1) | ((n & 0x55555555) << 1);  
    n = ((n & 0xcccccccc) >>> 2) | ((n & 0x33333333) << 2);  
    n = ((n & 0xf0f0f0f0) >>> 4) | ((n & 0x0f0f0f0f) << 4);  
    n = ((n & 0xff00ff00) >>> 8) | ((n & 0x00ff00ff) << 8);  
    n = (n >>> 16) | (n << 16);  
    return n;  
}
5.2.3 逻辑实现
我们先使用 8 个二进制位置进行说明:abcd efgh.
- 先进行 1 VS 1的翻转:badc fehg.
- 然后进行 2 VS 2的翻转:dcba hgef.
- 然后进行 4 VS 4的翻转:hgef dcba.
- 然后可以将其拓展到 int(32位)的情况下.

具体说明如何实现:如何将 1 VS 1 实现翻转:依然是:abcd efgh.
- 先将 abcd efgh按位与&上10101010这样最后的结果是:a0c0 e0g0.
- 然后将 abcd efgh按位与&上01010101这样最后的结果是:0b0d 0f0h.
- 将 a0c0 e0g0进行>>> 1运算, 最后结果是:0a0c0e0g
- 将 0b0d 0f0h进行<< 1运算, 最后结果是:b0d0f0h0
- 最后将两个结果进行 按位或( | )运算, 最后结果:badc fehg.
- 因为我们使用的是 8个二进制位的, 将其扩展到32个二进制位,1010对应的是:十六进制的a,0101对应的是十六进制的 5. 所以扩展到32位是:
 n = ((n & 0 xaaaaaaaa) >>> 1) | ((n & 0 x 55555555) << 1);.

如何实现:2 VS 2 的翻转:此时 n 的状态是:badc fehg.
- 先将 badc fehg按位与&上11001100, 最后结果是:ba00 fe00.
- 然后将 badc fehg按位与&上00110011, 最后结果是:00dc 00hg.
- 然后将 ba00 fe00进行>>> 2操作, 结果:00ba 00fe.
- 然后将 00dc 00hg进行<< 2操作, 结果:dc00 hg00.
- 最后进行按位或 ( | )运算, 结果:dcba hgfe.
- 因为我们使用的是 8个二进制位, 所以扩展到32个二进制位,1100对应的十六进制是:c,0011对应的十六进制是:3.

如何实现:4 VS 4 的翻转:此时 n 的状态是:dcba hgfe.
- 先将 dcba hgfe按位与&上1111 0000, 最后结果是:dcba 0000.
- 然后将 dcba hgfe按位与&上0000 1111, 最后结果是:0000 hgfe.
- 然后将 dcba 0000进行>>> 4操作, 结果:0000 dcba.
- 然后将 0000 hgfe进行<< 4操作, 结果:hgfe 0000.
- 最后进行按位或 ( | )运算, 结果:hgfe dcba.
- 因为我们使用的是 8个二进制位, 所以扩展到32个二进制位,1111对应的十六进制是:f,0000对应的十六进制是:0.
之后的 8 VS 8 的就不进行说明了, 经过前面的推导, 后续的实现肯定是能进行的, 自己画一下吧.
6. 二进制中有几个 1
 
6.1 逻辑实现
我们还是按照 8 个二进制位进行举例子:1111 1001, 我们定义一个长度, 统计每一个长度中的 1 的个数, 假设现在长度是 1,
- 那么 0位置的1个数有1个,
- 1位置- 1的个数有- 0个,
- 2位置- 1的个数有- 0个
- 3位置- 1的个数有- 1个
- 4位置- 1的个数有- 1个
- 5位置- 1的个数有- 1个
- 6位置- 1的个数有- 1个
- 7位置- 1的个数有- 1个
然后我们进行扩展, 将现在的长度变为:2, 那么:11 11 10 01
- 1位置- 1的个数有- 1个,
- 2位置- 1的个数有- 1个
- 3位置- 1的个数有- 2个
- 4位置- 1的个数有- 2个
用代码实现将 1 长度变为 2 长度
先将 1111 1001 & 0101 0101
最后结果是:01010001
然后我们将 1111 1001 >>> 1 -> 0111 1100 & 0101 0101
最后结果是:01010100
然后将两个状态相加:
1010 0101   此时就成了长度为 2 的情况下, 二进制中 1 的个数
10 10 01 01 
2  2  1  1   长度为 2 的情况下, 二进制中 1 的个数
然后继续使用代码表示将 2 长度迁移到 4 长度
1010 0101 这个是长度为 2 的情况下的表示
我们将 1010 0101 & 0011 0011
0010 0001 结果
然后我们将 1010 0101 >>> 2 -> 0010 1001 & 0011 0011
0010 0001 结果
将两个结果加起来
0100 0010     此时是长度为 4 的情况下, 二进制中 1 的个数
然后继续用代码表示将 4 长度迁移到 8 长度
0100 0010 这个是长度为 4 的情况下的表示
我们将 0100 0010 & 0000 1111
0000 0010 结果
然后将 0100 0010 >>> 4 -> 0000 0100 & 0000 1111
0000 0100 结果
将两个结果相加:
0000 0110 这个是长度为 8 的情况下, 二进制中 1 的个数,此时是:6 个.(2^2 + 2^1 == 6).
以此类推, 一直将长度迁移到了 int(32位) 的情况下就是最后的结果
6.2 代码实例
public static int cntOnes(int n) {  
    n = (n & 0x55555555) + ((n >>> 1) & 0x55555555);  迁移到长度为 2
    n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);  迁移到长度为 4 
    n = (n & 0x0f0f0f0f) + ((n >>> 4) & 0x0f0f0f0f);  迁移到长度为 8
    n = (n & 0x00ff00ff) + ((n >>> 8) & 0x00ff00ff);  迁移到长度为 16
    n = (n & 0x0000ffff) + ((n >>> 16) & 0x0000ffff); 迁移到长度为 32, 就是最后结果
    return n;  最后结果
}
另一个代码实例:这个用了 Brian Kernighan算法, 这个题目让统计一个数字中 32个 二进制位中所有 1 的数量,
- 所以我们直接将数字中最右侧的 1提取出来, 然后用原来的数字减掉, 此时设置一个计数器cnts让cnts++,
- 然后继续利用 Brian Kernighan算法, 继续减掉, 直到数字变成0停止, 这样cnts的值就是一个数字中所有二进制位中1的数量.
public static int cntOnes(int n) {  
    int cnts = 0;  
  
    while (n != 0) {  
       n -= (n & -n);  
       cnts++;  
    }  
  
    return cnts;  
}
7. 学习位运算的意义

位运算的常数时间是非常好的, 使用位运算可以很大程度上提高我们代码的运行速度, 而且使用的内存也很少, 在一些底层的操作上, 使用位运算是极好的. 而且位运算使用起来也会很简洁高效,
但是我们也没有必要强制使用位运算, 没有必要去在任何情况下都追求位运算的实现和使用, 尽量写到时间复杂度和空间复杂度最优, 自己能理解就行. 不要钻牛角尖.
大牛的实现我们直接当成模板用就行了,



















