0. 常见位运算及操作简介
- & : 与运算,两个数全为1结果为1,否则结果为0
- | : 或运算,两个数全为0结果为0,否则结果为1
- ~:非运算,1变为0,0变为1
- ^:异或运算,相同为0,相异为1
- " >>" 和 “>>>” , 算术右移和逻辑右移,相当于除以2
A = 11111111111111111111111110000001
B = 2
A >> B = 11111111111111111111111111100000
A >>> B = 00111111111111111111111111100000
- “<<” 相当于乘以2
- 技巧一:n & (n-1) 去掉最后一位的1
- 技巧二: a ^ b ^b = a
- 技巧三: n & (-n) 得到最后一位的1
1. 求二进制中1的个数
题目:给出一个整数n,求其二进制表示中1的个数。
思路:1.每次根据技巧一去掉一位1,进行统计;2.定义一个mask与原数的每一位进行与运算
// 求一个数中1的个数
public static int numberOfOne1(int n){
// 此方法会改变输入数据的值
int cnt = 0;
while(n != 0){
cnt++;
//去掉最后一个1
n &= n-1;
}
return cnt;
}
//求一个数中1的个数
public static int numberOfOne2(int n){
// 此方法不会改变输入数据值
int cnt = 0;
int mask = 1;
while(mask != 0){
if((n & mask) != 0){
cnt ++;
}
mask <<= 1;
}
return cnt;
}
2. 判断一个数是否为2的幂或4的幂
题目:判断一个数是否为2的幂或者为4的幂
思路:1.如果为2的幂则二进制表示中1仅可出现一次;2.如果为4的幂,首先是2的幂,然后需要保证二进制中1的位置在偶数位上;
//判断一个数是否是2的幂
public static boolean isPowOfTwo(int n){
return (n != 0 && (n & (n - 1)) == 0);
}
//判断一个数是否是4的幂
public static boolean isPowOfFour(int n){
//先判断是否是2的幂
//然后判断1所在的位置是否在偶数位上
return ((n & (n - 1)) == 0) && n >0 && ((n & 0x55555555) != 0);
//数学理论如下
//依据 #1: (4^n-1) = (4-1) (4^(n-1) + 4^(n-2) + 4^(n-3) + ….. + 4 + 1)
//依据 #2 4^(n+1) - 1 = 4*4^n -1 = 3*4^n + 4^n-1
// return n > 0 && (n & (n - 1)) == 0) && ((n - 1) % 3 == 0);
}
3. 位运算实现两数交换
问题:在不借助辅助变量的前提下进行两个变量之间的交换
思路:1.采用异或运算进行;2.采用 加减法运算进行
//位运算实现两数交换,在不使用中间变量前提下
public static void change(int a , int b){
a = a ^ b;
b = a ^ b;
a = a ^ b;
// 使用加法实现
// a = a + b;
// b = a - b;
// a = a - b;
// 使用减法实现
a = a - b;
b = a + b;
a = b - a;
}
4. 找出唯一出现一次的数(1个数版本和2个数版本)
问题:一个整数数组中,仅有一个(两个)数出现一次,其他数都出现2次,找出这个数(这两个数)
思路:1.使用异或运算技巧二,分别对所有数异或,最后的数即为所要找的数;
2. 首先利用异或运算得到两个出现一次数的异或结果,然后根据其中某位的1将数组分为两部分(如果异或后的结果某位为1则表示所要找的两个数在该位是不同的,这个划分的依据),然后分别对两个数组求出现一次的数。
// 寻找一个数组中仅出现一次的数,这个数只有一个
public static int findOnceAppearNumber(int[] nums, int len){
int result = nums[0];
for(int i = 1; i < len; i++){
result ^= nums[i];
}
return result;
}
// 寻找一个数组中仅出现一次的数,这个数有两个
public static int[] findOnceAppearNumber2(int[] nums, int len){
int[] result = new int[2];
int tmp = nums[0];
for(int i = 1; i < len; i++){
tmp ^= nums[i];
}
// 得到最后一位的1,并基于此将数组分为两部分
int bits = tmp & (- tmp);
List<Integer> arrays1 = new ArrayList<>();
List<Integer> arrays2 = new ArrayList<>();
for(int i = 0; i < len; i++){
if((nums[i] & bits) == 0){
arrays1.add(nums[i]);
}else{
arrays2.add(nums[i]);
}
}
// 分别在两个数组中寻找出现一次的数
// 此处可以直接调用出现一次的函数
tmp = arrays1.get(0);
for(int i = 1; i < arrays1.size(); i++){
tmp ^= arrays1.get(i);
}
result[0] = tmp;
// result[0] = findOnceAppearNumber(arrays1, array1.size());
tmp = arrays2.get(0);
for(int i = 1; i < arrays2.size(); i++){
tmp ^= arrays2.get(i);
}
result[1] = tmp;
// result[1] = findOnceAppearNumber(arrays2, array2.size());
return result;
}
5. 位运算实现加法和减法
问题:使用位运算实现加法和减法
思路:1.对于加法可以手动模拟运算,先进行无进位的加法,然后计算进位,然后循环计算无进位加法结果和进位结果,直到进位为0为止;
2.减法运算,相当于加上一个负数,所以要先求减数的补码,然后调用加法运算;
// 使用运运算实现加法
public static int addByBit(int x, int y){
// // 递归实现
// if(y == 0){
// return x;
// }
// // 进行不进位加法
// int sum = x ^ y;
// // 求进位
// int carry = (x & y) << 1;
// // 进位与不进位结果相加
// return addByBit(sum, carry);
// 迭代实现
int sum = x ^ y;
int carry = (x & y) << 1;
while(carry != 0) {
int a = sum;
int b = carry;
sum = a ^ b;
carry = (a & b) << 1;
}
return sum;
}
// x:减数;y:被减数
public static int substractByBit(int x, int y){
// 先求减数的补码(除符号位外取反加1)
int sub = addByBit(~y, 1);
// 调用上面加法运算
return addByBit(x, sub);
}
6. 输入两数m,n,计算需要改变多少位能使m变成n
问题:给定两个整数,求需要改变多少位使得两个数相同
思路:利用异或运算,只需要改变不同位即可,相当于进行异或运算后求其1的个数
// 给定两个整数,求改变多少位可以使得两个数相同
public static int changeCount(int n, int m){
int cnt = 0;
int tmp = n ^ m;
while(tmp != 0){
cnt ++;
tmp &= (tmp - 1);
}
return cnt;
}
7. 计算[0,n]上各个数的1的个数
问题:输入整数n, 计算0,n中所有数的二进制表示中1的个数,返回一个记录个数的数组
思路:1. 一遍遍历,针对每个数都计算其二进制1的个数;
2. 优化版本,一个数的二进制1个数等于去掉一个数最后一位1所对应的数的个数加1。即:
count[i] = count[i & (i-1)] + 1;
// 计算0,n上各个数的二进制表示中1的个数
public static int[] findArrayNumberOfOne(int n){
// //直接遍历的方式
// int[] result = new int[n+1];
// for(int i = 0; i <= n; i++){
// result[i] = numberOfOne1(i);
// }
// 优化方式
int[] result = new int[n+1];
// 初始化第一个数
result[0] = 0;
for(int i = 1; i <= n; i++){
result[i] = result[i & (i-1)] + 1;
}
return result;
}
8. 位运算解决N皇后问题
问题:N皇后问题
思路:采用位运算
/**
*
* @param row: 当前所在的行数
* @param col:所有的列
* @param pie: 左下角位置
* @param na: 右下角位置,即不允许皇后在一条斜率为1或者-1的斜线上
*/
public static void nqueen1(int row, int col ,int pie, int na){
// 递归终止条件
if(row >= NQUEEN){
count ++;
return;
}
// 选取一行中可以防止皇后的位置
int bits = (~(col|pie|na)) & ((1 << NQUEEN)-1);
while(bits > 0){
// 在能够放置皇后的位置中选择一个位置,每次从最后的位置选取
int p = bits & -bits;
// 往下一层递归
nqueen1(row+1, (col|p), (pie|p) << 1, (na|p) >>1);
// 将该行可防止皇后的最后一位去掉,表示该位置已经尝试过
bits &= bits - 1;
}
}
9. bitmap
位图的使用也与位运算紧密相关,可以用于一些大数据处理场景,例如:
- 判断一个数是否在一个集合中
对于一个数仅需要保存其是否存在,只需要两个状态,所以一个位就能满足条件,这样一个int类型的数可以表示32个数的状态,可以把集合中的数通过位运算表示其状态,然后判断一个数是否存在其中。里面涉及到一些简单的移位操作,可以自行搜索。 - 布隆过滤器
判断一个数是否在一个集合中。原理是对于一个数通过若干个哈希函数得到若干个哈希值,将这些哈希值表示在位图中。如果一个数在该集合中,那么其所有哈希值的二进制位都应该是1;如果一个数不在该集合中,原则上其所有哈希值得二进制位不全为1,但是会有误差,因为可能是别的数字存在的缘故导致原本为0的位变为了1,这与位图的大小、哈希函数和哈希函数的个数有关系。感兴趣可以自行搜索。