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