力扣题目描述:
问题描述
一开始接触到二进制有关的运算什么与、或、异或等,每次都去看题解很不甘心,决心花一些时间整理一下,虽然这道题目有很多的解法,但主要记录二进制分治,只因其空间时间复杂度均为O(1),何况Java的int类型包装类中就是试用了二进制分治,这还是值得一学。
要彻底弄懂二进制分治,首先得了解这几个运算符合运算特性:&,|,&&,||;这几个的却别
&:通路与,是一个二元运算符,其运算规则是 x & 1 = x, x & 0 = 0;就拿a & b 来说,先判断a的值不论是否为真假,还要判断b的真假。两个为真结果才为真。而&&的区别就在于,&&是短路与,例如 a && b,只要a为假,就立即返回假,不会去判断b,而a为真是才去判断b是否为真。x | 0 = x
、x | 1 = 1,其思路也和上面差不多;
官方的分治是这样写的,可是为什么这样写,官方也没有太多的解释,接下来我们仔细研究其原理和步骤。
public class Solution {
private static final int M1 = 0x55555555; // 01010101010101010101010101010101
private static final int M2 = 0x33333333; // 00110011001100110011001100110011
private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111
public int reverseBits(int n) {
n = n >>> 1 & M1 | (n & M1) << 1;
n = n >>> 2 & M2 | (n & M2) << 2;
n = n >>> 4 & M4 | (n & M4) << 4;
n = n >>> 8 & M8 | (n & M8) << 8;
return n >>> 16 | n << 16;
}
}
原因分析:
1、M1~M4如何得来?为什么是这几个数?别的可不可以?
回答这几个问题之前我们得了解二进分治思想:这里额交换必须是1,2,4,8,16……
简单来说其实就是:1位交换(奇偶数位交换)
2位交换
4位交换
……
终止条件是位数的一半,例如以上代码块中,32位二进制数终止条件就是16,只需要进行到16位的交换即可,二进制用移位实现。
我们用大家最熟悉的十进制模拟一下,看看能不能颠倒数字位。
所以到这里知道M1~M4的取值了吧,就是2的n次方(n==位数的一半,前半部分与后半部分进行一次交换)
所以官方的答案改成这样,也没有问题
public class Solution {
private static final int M1 = 0x55555555; // 01010101010101010101010101010101
private static final int M2 = 0x33333333; // 00110011001100110011001100110011
private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111
private static final int M16 = 0xffffffff;// 11111111111111111111111111111111
public int reverseBits(int n) {
n = (n >>> 1) & M1 | (n & M1) << 1;
n = (n >>> 2) & M2 | (n & M2) << 2;
n = (n >>> 4) & M4 | (n & M4) << 4;
n = (n >>> 8) & M8 | (n & M8) << 8;
n = (n >>> 16) & M16 | (n & M16) << 16;
return n;
}
}
- 现在我们来解读第一句代码:
n = (n & 0x55555555) << 1 | ((n >> 1) & 0x55555555);
0x55555555
变成二进制是0101 0101 0101 ...,其他的代码块上有,我就只拿这一个来分析。n & 0x55555555
是取其奇数位,为什么呢?是不是忘记了&计算规则,x & 1 = x,x & 0 = 0;说来这么多,还不如画个图来得简单!
为其方便性我取4位做了分析
因为这是4位,故需要再交换一次2位就完成颠倒,我就不画了,有兴趣的小伙伴可以画画加深印象。
画完后就会发现思路其实很简单:
- 保留奇数位,偶数位置零(否者不为0,在奇数位移动到偶数位时,偶数位不为零的又移动到了奇数位影响后面|运算),移动奇数位置偶数位(左移一位);
- 移动偶数位置奇数位,再一次保留奇数位,偶数位置零。
- 将以上两个步骤的值异或 |,即可完成一位的交换(奇偶数位交换)
从自顶向上的角度看,我们把整个序列的翻转分成了:隔1位交换二进制数、隔2位交换二进制数、隔4位交换二进制数、隔8位交换二进制数、隔16位交换二进制数五个子问题,属于分治法思想。
public int reverseBits(int n) {
逐一移位
int res = 0;
for (int i = 0; i < 32 && n != 0; i++) {
res |= (n & 1) << (31 - i);
n = n >>> 1;
}
return res;
拓展:
统计1的数:第一步保留奇数位偶数位置零,偶数位移动至奇数位,再次保留奇数位,偶数位置零,合并。(合并最低位的1)
uint32_t getNum(uint32_t n) {
n = (n & 0x55555555) + ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);
n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);
n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
return n;
}
参考博客:https://blog.csdn.net/qq_40950183/article/details/117389310