以int类型为例,位运算只需单独考虑对应的每一位即可。
4k^(4k+1)^(4k+2)^(4k+3) = 0
力扣题目:371. 两整数之和
public static int add(int num1, int num2) {
int sum, carry;
do {
//两个数异或,二进制的相加,但不进位
//比如:01010 ^ 00101 = 01111
//因为不涉及到进位,01010 + 00101结果就是01111
sum = num1 ^ num2;
//计算进位的部分,两个bit都是1才会产生进位,用按位与
//是向高位进位,因此左移1位
carry = (num1 & num2) << 1;
num1 = sum;
num2 = carry;
} while (carry != 0); //只要计算中产生了进位,还得继续把进位部分累加进来
return sum;
}
/**
* 稍微简化一下
*/
public int getSum(int a, int b) {
//按位异或 其实就是不进位的加法
int ans = a;
while(b != 0) {
int carry = (ans&b)<<1;//计算出该进位的部分,暂存
ans = ans^b;//再通过异或,计算不进位的相加结果
b = carry;
}
return ans;
}
/**
* 更简洁的递归写法
* 异或可以看成是不进位的相加
*/
public int add(int a, int b) {
if(a==0)return b;
if(b==0)return a;
// 前一项是不进位的相加结果
// 后一项是进位,进位最后始终会变成0
return add(a^b, (a&b)<<1);
}
关于异或(^),有一种可能更直观的理解:两个数的二进制位,按最低位对齐(高位补0)之后,分别进行按位相加(但忽略进位)的结果。
关于异或运算有2个性质:
a ^ a = 0
0 ^ a = a
```java
a = a ^ b;
b = a ^ b; // (a^b)^b = a ^ (b^b) = a ^ 0 = a
a = a ^ b; // (a^b)^a = a^a ^ b = 0^b = b;
```
(不过,个人倒是觉得没必要用这种方式来交换两个数)
《linux多线程服务端编程》501页分析了,用异或运算交换变量,是错误的行为。并且不能加快运算,也不能节省内存。
参考:用异或来交换两个变量是错误的https://blog.csdn.net/solstice/article/details/5166912
//判断最低一个bit是1(奇数)还是0(偶数)
return (n&1)==1;//true: 奇数;false: 偶数
2*n 等价于 n<<1
2*n+1等价于 n<<1|1
n/2 等价于 n>>1
比如,在二分查找中一个惯用的技巧,即求[left, right]的中间位置:
int mid = (left+right)>>1;
不过,为了防止相加后溢出,超出int的表示范围,最好这样写:
int mid = left + ((right - left)>>1); // 这里必须加括号!
N&(N-1)
是啥?N&(~N+1)
是什么?N&(-N)
是啥?这3者的关键是要先理解N&(N-1)
。N-1
是将二级制的N,其最右边部分的10..0(其中0可以有0到多个)
,打散为01..1
(其中1的个数和前边0的个数相同)。那么 N&(N-1)
的结果就是将N的二进制形式中,最低位的1抹掉后的数。
`N&(~N+1)`和`N&(-N)`,这两者其实是等价的。他们的结果是,将`二级制的N,其最右边一个为1的位保留成1,其他位全变成0`的数。当然这是N不为0的情况。
如果N=0呢?
```java
N&(~N+1) = 0 & (-1+1) = 0 //事实上,0与任何数按位与结果是0
```
备注:
java代码验证: -N == (~N+1)
int val = Integer.MAX_VALUE; assert -val == (~val+1); val = Integer.MIN_VALUE; assert -val != (~val+1); for(int i=-1000;i<1000;i++) { assert -i != (~i+1); }
-N == (~N+1)其实是有原因的,java在表示负数的时候,用的是补码,补码就是由相应的正数值按位取反(得到反码),再加1得出。
因此,如果N是一个正数,从二进制形式看,~N+1(即N的补码)就是用来表示-N这个负数的。
//以byte类型为例,来体会一下补码 01111111 //127 00000000 //0 11111111 //-1 由1的二进制0000_0001按位取反1111_1110,再加1得出 10000001 //-127 127的二级制0111_1111按位取反1000_0000,再加1得出 10000000 //-128 128的二级制1000_0000按位取反0111_1111,再加1得出 byte的表示范围为:[-128, 127] Integer.MIN_VALUE == 0x80000000 (即1<<31) Integer.MAX_VALUE == 0x7fffffff
n>>3
是计算n/8的值,那n%8可以用位运算代替么?可以!对于16,32,…,512等2^x
的数同理n%8 等于 n&7 //即取二进制n的低3位
n>0 && (n& (n-1)) == 0
等价于n为2的某次方。(其中n为正整数)如何理解:
n为2的某次方,等价于n的二进制形式中有且仅有1个1,其余bit为0,那么n& (n-1)) 必然为0。
反过来,如果n不为2的某次方,n的二进制形式中至少会有2个1,那么n-1是将n的二进制中最后1个1打散,即xx..x100..0
变成xx..x011..1
。n& (n-1))必不为0(只是将最低位的1变成了0,至少还留有高位的1)。
小结一下:
** n = n & (n-1)的结果是抹掉n(二进制形式中)最低位的1**
** n = n &(~n+1)的结果是,只保留n二进制中最低位的1,其他位全变为0**
备注:这是力扣第231题:231. 二的幂
遍历一个整数的二进制形式中,所有为1的bit组成的集合的子集。
第一种,通过for循环:
int mask = 0b10101;
// 第一种:因为有i>0条件,遍历结果中至少含有1个bit的1
for (int i = mask; i > 0; i = (i - 1) & mask) {
System.out.println(Integer.toBinaryString(i));
}
/**输出结果为:
* 101010
* 101000
* 100010
* 100000
* 1010
* 1000
* 10
*/
第二种,通过do{}while()
循环:
int mask = 0b10101;
int subset = mask;
// 遍历mask的所有子集,包括不含1(即全0)的情况
do {
System.out.println(Integer.toBinaryString(subset));
subset = (subset - 1) & mask;
} while (subset != mask);
/**输出结果为:
* 101010
* 101000
* 100010
* 100000
* 1010
* 1000
* 10
* 0
*/
解释:第二种遍历,当subset
减为0之后,subset-1
为-1
,而-1
的二进制位全为1,-1 & mask
之后就等于mask
了,于是while退出。
笔者最早接触掩码这个词,应该是IP地址的子网掩码。这里说的掩码也是同一个意思。
一句话来描述掩码的含义和用途:一个操作数和提前设计好的某个掩码值进行按位与运算,能够得到需要的结果。
补充一句:将内网IP地址和子网掩码进行按位与运算能够得出网络标识(网络号)。
//举一个简单的例子: 我要判断一个byte数值的某个bit是0还是1
//先定义好各个bit的掩码
public static final byte[] MASK = {
(byte)(1<<7),
1<<6,
1<<5,
1<<4,
1<<3,
1<<2,
1<<1,
1<<0,
};
public static boolean isSet(byte val, int index) {
int mask = MASK[index&7];//index的有效范围是0~7
return (val&mask) == mask;
}
//比如,24这个数
byte val = 0b0001_1000;//24
System.out.println(isSet(val,0));//false,最高位,第0位不是1
System.out.println(isSet(val,3));//true,第3位是1(下标从0开始数的)
System.out.println(isSet(val,4));//true,第4位是1(下标从0开始数的)
ps: 这是力扣第191题:191. 位1的个数
//第一种写法,每次判断最低一位是否为1,为1计数加1,
//然后n无符号右移1位,再次进行判断;直到n变为0。
public static int bit1Counts(int n) {
int rc = 0;
while(n!=0) {
if((n&1) == 1) {
rc++;
}
n>>>=1;
}
return rc;
}
//第二种写法,循环次数比前一种实现要少(前一种是所有位中的0和1都判断了,这种只判断为1的位)
public static int bit1Counts2(int n) {
int count = 0;
int right1;
while(n!=0) {
//取到最右边的1,其他位保留0
right1 = n & (~n + 1);
count++;
//再把n最右边的1变成0,循环继续
n ^= right1;
}
return count;
}
//第三种写法,简单直接,最优解
public static int bit1Counts3(int n) {
int count = 0;
while (n != 0) {
//参考上文第5点n&(n-1)的理解:
//n-1是把n的最右(低)位的1“打散”开来,
//因此,n&(n-1)会将n的最低位的1抵消掉
n &= (n - 1);
count++;
}
return count;
}
// 第四种写法:每4位统计一次数量
public int bit1Counts4(int i) {
//bit是0到15转化为二进制时,其中1的个数
int bit[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
int count = 0;
while (i != 0) {
//通过每4位计算一次,求出包含1的个数
count += bit[i & 0xf];
// i无符号右移4位
i >>>= 4;
}
return count;
}
思路:将所有的数异或在一起,得到的结果即为要找的数。
还是异或的性质,相同2个数异或结果为0,0和一个数异或结果是这个数本身。
解决n皇后问题:在n*n的棋盘上,放置n个“皇后”,要求棋盘上每一条横线、竖线、斜线(包括向左下方向的斜线,向右下方向的斜线)上都至多只有一个皇后,问有多少种放法。
求解这道题的算法是**O(n^n)
的时间复杂度**。没错,就是这么高的复杂度!!所以,一般求解这个问题的时候,n不会很大。准确地说,n会很小。请脑补一下,如果n达到32,求32的32次方将会是一个多大的数。
关于时间复杂度,可以这么来理解:
这里请位运算压轴出场来解决这问题,这种解法能极大地优化算法执行的常数时间,绝对是最优解。
整个思路是从上往下,一行一行地尝试放置皇后。请看以下代码和注释。
class Solution {
//求n皇后问题有多少种放法
public int totalNQueens(int n) {
int total = 0;
//假设我们只求解n<=32的情况
if(n>=1 && n<=32) {
//limit的二进制表示中,低n位为1,高位为0(可以对应到棋盘中的一行)
int limit = n == 32 ? -1 : ((1 << n) - 1);
total = process1(limit, 0, 0, 0);
}
return total;
}
// limit: 问题规模为n,limit低n位(bit)为1,其他位为0
// leftMask: 记录左斜线方向上是否有限制,并通过左移位将这种限制往更下一行进行传递
// rightMask:记录右斜线方向上是否有限制,并通过右移位将这种限制往更下一行进行传递
// colMask: 记录列方向上被占用的情况(二级制为1的位已被占用)
public int process1(int limit, int leftMask, int colMask, int rightMask) {
if(colMask==limit) {//所有列都已被占,说明n个皇后放置结束,得到一种方案
return 1;
}
int res = 0;
int pos = limit & ~(leftMask|colMask|rightMask);//得到哪些位置可以放皇后
while(pos!=0) {
//在pos不为0的情况下,找到pos中所有为1的位,进行放置皇后的尝试
//找到pos中最有低位的1,记为mostRightOne
int mostRightOne = pos & (~pos+1);
//在mostRightOne的位置放皇后
pos ^= mostRightOne;//等价于pos -= mostRightOne
//继续尝试下一行放置,其leftMask变为(leftMask|mostRightOne)<<1
//左移是告诉下一行放置皇后的时候不能占用这个位置
//否则跟本行放置的皇后在左斜线上有冲突
//rightMask同理
res += process1(limit, (leftMask|mostRightOne)<<1,
colMask|mostRightOne, (rightMask|mostRightOne)>>>1);
}
return res;
}
}
位运算是计算机里面最快的操作,没有之一。
就因为快,位运算受到很多算法爱好者的热捧。
位运算为什么快呢?
通常在编程的时候用到的位运算有哪些呢?以编程语言java
为例,有6种。
与(and)
0b0011 & 0b0010 => 0b0010
或(or)
0b0011 | 0b0010 => 0b0011
非(not)
~0b0011 => 0b1100
异或(xor)
0b0011 ^ 0b0010 => 0b0001
左移
0b0011<<2 => 0b1100
// 补充一点,java中左移超过31位的话,实际只移动对32取模的位数
System.out.println(1<<31);// -2147483648
System.out.println(1<<32);//1,左移32%32=0位
System.out.println(1<<33);//2,左移33%32=1位
System.out.println(1<<34);//4,左移34%32=2位
右移
右移会导致低位被丢弃。
无符号右移(>>>)
用来将一个数的各二进制位全部右移指定的位数,左补0(左边最高位是符号位,为0表示正数)。
eg. 0b0011>>>1 => 0b0001
带符号右移(>>)
用来将一个数的各二进制位全部右移指定的位数,左补1(如果原数最左位是1,即为负数),或者补0(如果原数最左位是0)。
eg1. 0b1100>>1 => 0b1110
eg2. 0b0100>>2 => 0b0010
怎么样,觉得有收获的话,在下方给作者一个赞吧!
感谢阅读。