今天来总结一下Java中的位运算,并介绍几个位运算的骚操作,请坐稳扶好,下面发车😄
文章目录
1. 位运算基本介绍
1.1. 位运算适用的数据类型
- 在Java中,位运算适用于基础数据类型中的整数类型,即byte,short,char,int,long ,引用类型,布尔类型,浮点类型不可以使用。
- byte,short,char类型在使用位运算的时候会被转换为int类型(符号位扩展),其结果也是int类型(表达式中没有long);
- 含有long类型的位运算的表达式的结果是long类型
我这里就不一一验证了哈。
1.2. 位运算介绍
在说位运算之前,我们先看一下逻辑运算,逻辑运算是离散数学中的概念,我们在高中的时候应该也学过,或,且,非等运算,举两个例子:
- 逻辑与,a且b,它的结果是,一假则假,a,b只要有一个是假,结果就是假的,否则是真
- 逻辑或,a或b,它的结果是,一真则真,a,b只要有一个是真,结果就是真的,否则是假
位运算跟逻辑运算是相通的,把每一位的0看作假,1看作真,在这样的基础上进行的逻辑运算。因此,
- 按位与(&),如果两位都是1,结果是1,否则是0;
- 按位或(|),如果两位都是0,结果是0,否则是1;
- 按位取反(~),如果某一位是1,结果是0,否则结果是1;
- 按位异或(^),如果两位代表的数值一样,则结果是0,否则是1。
- 左移操作,a<<m,表示把整数a,左移m位,m可以是任何整数类型,可以是0,负整数,正整数
- 当m等于0时,相当于什么也没做
- 当a是int类型时,(byte,char,short会自动扩展成int)
- m大于0,且m小于32,相当于把a按位左移m位,低位补0,高位舍去。
- m大于等于32,int类型4字节,32位,左移32位,相当于啥也没干,因此当m大于32时的结果,相当于a<<(m%32)的结果;
- m小于0,且m大于-32,例如m=-1,左移-1位,相当于左移32位时,上一次的结果,即左移31位的结果,此时结果相当于a<<(32+m)的结果
- m小于等于-32,int类型4字节,32位,左移-32位,相当于啥也没干,因此当m小于-32时的结果,相当于a<<(32-(-m)%32)的结果
- 当a时long类型时,只需把结论中的32换成64即可,不再赘述
- 右移操作,a>>m,相当于把a按位右移m位,低位舍去,高位补符号位(啥是符号位?)。剩余的规则跟左移是一样的。
- 无符号右移,可以理解为普通的右移,只不过高位补0
1.3. 对异或的理解
异或规则是,如果两位代表的数值一样,则结果是0,否则是1。可以理解为什么呢?可以理解为,如果两个位中1的个数是偶数,则结果为0,否则结果为1。
既然,异或可以这么理解,那么多个数同时异或的时候,异或的结果假设为ans,即
a1 ^ a2 ^ a3 ^.....an = ans
那ans的每一位,都是所有的数在这一位上的1的个数的奇偶性决定的,如果所有的数在这一位上1的个数加起来是奇数,那ans的这一位就是1,否则是0。
而结果中1的个数的奇偶性,跟异或的顺序是没有关系的,不可能说,a1和a3先异或,就会导致结果不一样,因此异或操作满足交换律和结合律。
有几个显而易见的小结论:
- 当一个数和自己异或的时候,a^a = 0,
- 一个数和0异或的时候,a^0 = a;
- 当一个数和-1异或的时候,a^-1 = ~a,
那异或还可以理解为什么呢?
可以理解为无进位相加,a^b相当于a和b按位无进位相加,无进位相加,就是按照加法的规则,一位一位的相加,只不过,不考虑进位;
那a+b的结果相当于a和b无进位相加的结果,加上进位信息,进位信息怎么算?
啥时候产生进位??,只有两位都是1的时候会产生,因此a和b的进位信息就是(a&b)<<1,为啥左移一位?因为进位是向上进位的。因此可以使用位运算实现加法。代码如下:
public int add(int a, int b) {
int t;
while (b != 0) {
t = a;//t 先抓住上一次的a
a ^= b;// a保留a和b无进位相加的结果
b = (b & t) << 1;//b和原来的a相与,保留进位信息
}
return a;
}
2. 位运算应用
2.1. 快速乘法
a*b,当b是2的n次幂时,即 b = 2 n b =2^n b=2n,a*b = a<<n;
2.2. 快速除法
a b \frac{a}{b} ba,当b是2的n次幂时,即 b = 2 n b =2^n b=2n, a b \frac{a}{b} ba = a>>n;
2.3. 快速取模
a%b,当b是2的n次幂时,即
b
=
2
n
b =2^n
b=2n,a%b = a&(b-1);为啥,因为
b
=
2
n
b =2^n
b=2n,因此b的二进制中,第
n
n
n位为1,而
b
−
1
=
1
−
2
n
1
−
2
=
2
0
+
2
1
+
2
2
+
.
.
.
+
2
n
−
1
b-1 =\frac{1-2^n}{1-2}=2^0+2^1+2^2+...+2^{n-1}
b−1=1−21−2n=20+21+22+...+2n−1(等比数列求和),相当于把第n位置0,后面的所有位都置1。
而a%b不就是,a中去掉几个x个b,剩下不够一个b的部分吗?如果a<b,那么a<=b-1,a和全都是1的部分相与,那结果显然正确;如果a>b,那a至少第n位是1,有可能第n+1位,第n+2位是1,但是这些位代表的数,都是b的整倍数,剩下不足一个b的部分,就是a的低n-1位组成的数,才是余数,所以取a的低n-1位就是结果。
2.4. 对称加密
对于两个整数a,b,如果 a = a^b,此时a成了a和b异或的结果,a此时再次异或b,a = a^b^b = a^0 = a,又变回了a,此时b相当于一个对称密钥的作用。
2.5. 两数交换
不使用额外变量交换两个数,直接上代码:
public void swap(int a, int b) {
a ^= b; //此时 a的值是 a^b
b ^= a; // b = (a^b) ^b = a;
a ^= b; // a = (a^b) ^a(b已经变成a了) = b;
}
如果交换的是数组元素,代码如下:
public void swap(int[] arr, int i, int j) {
if (i != j) {
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
}
}
异或的两个元素不能同时指向同一块内存,否则这个内存的数变成0
2.6. 位图(bitmap)
假设需要实现一个集合,可以往集合中放入自然数,然后判断一个数是不是存在于集合中,我们可以使用hash表实现,当数的范围比较集中的时候,我们可以使用一个bit位是0或1表示这个数是否在集合中。代码如下:
public class BitMap {
int[] data;
int max;
/**
* @param max bitmap能保存的最大自然数
*/
public BitMap(int max) {
if (max < 0) throw new IllegalArgumentException("max number can not less than zero");
this.max = max;
data = new int[(max + 31) >> 5];
}
public void put(int number) {
if (number > max) throw new IllegalArgumentException("number can not greater than max");
// 先找到这个数应该存在哪个整数里,粗略定位
// 再确定这个数在哪一位上
//然后把这一位置1
data[number >> 5] |= (1 << (number & 31));
}
public void remove(int number) {
if (number > max) throw new IllegalArgumentException("number can not greater than max");
data[number >> 5] &= ~(1 << (number & 31));
}
public boolean contains(int number) {
if (number > max) throw new IllegalArgumentException("number can not greater than max");
return (data[number >> 5] & (1 << (number & 31))) != 0;
}
}
2.7. 确定一个int整数中有几个位是1
解法1: 依次测试每一位是0 还是1,然后设置一个计数器,当前位是1,则计数器加一,代码如下:
public int countOneBit(int number) {
int ans = 0;
for (int i = 0; i < 32; ++i)
if ((number & (1 << i)) != 0) ++ans;
return ans;
}
解法2: 解法1遍历的次数固定32次,还有更好的方法吗?有
下面我们先看一下,假设一个数x不是0,x-1的二进制表示与x的二进制表示有什么不同???
其实是这样的,因为x不是0,因此不妨假设从低位开始数,x的第一个非0的位在第
n
n
n(
0
<
=
n
<
=
31
0<=n<=31
0<=n<=31)位,那么x和-1相加,相当于把前低n位全部取反,剩余的,同时第n位和1相加,产生一个进位,导致后面的位和1相加以后,都没有发生改变。如下图所示:
x-1的结果,就是把x最右边的1及其前面的位都取反了,那x&(x-1),相当于,把x最右边的1置0,那么我们设置一个计数器,每一次把x最右边的1置0,然后计数器加一,直到x变成0为止,代码如下:
public int countOneBit(int number) {
int ans = 0;
while (number != 0) {
number &= (number - 1);
++ans;
}
return ans;
}
这个循环次数是,number中有几个1,就循环几次。
解法3: 有没有更好的做法?有,请往下看。
这种做法的思路是,先两位分组,把两位中1的数量找出来,还放到这两位中,如下图所示:
那如何做到这一步呢?两位二进制,一共四种情况,我们分别列出来看一下,如下图所示:
假设这个数是x,我们可以把x无符号右移一位,然后使用原来的x减去它,即x-(x>>>1);这样做并不可行,因为是整体无符号右移,不是两位一组的无符号右移,这样移动会导致下一组的最低位,移到这一组的最高位,如果这一位是0那道无所谓,如果是1,就影响计算了,因此,我们需要把x>>>1的值中所有的偶数位置0,因此只需要让它跟0x55555555
相与即可,如下图所示:
经过这一步,已经完成了两位一组的,把1的个数找出来,并存储在这两位当中了,接下来,就是4位一组,8位一组,16位一组进行合并即可。
四位一组进行合并,我们需要四位一组的高两位和低两位相加,我们把上一步得到的结果,记作x,我们让x无符号右移两位,这样,高两位和低两位对齐了,不要忘记清除垃圾数据,因此右移的过程中,下一组的低两位,会移动到当前组的高两位上,影响计算结果,因此计算之前,清除高两位的值,如下图所示:
八位一组进行合并,跟四位一组合并类似,也是需要移位,把最高位置0,下面不再赘述,当16位一组合并完成后,因为int类型有32位,1的个数不会超过2的5次方,所以取结果的低6位即可,代码如下:
public int countOneBit(int number) {
number -= ((number >>> 1) & 0x55555555);
number = (number & (0x33333333)) + ((number >>> 2) & 0x33333333);
number = (number + (number >>> 4)) & 0x0f0f0f0f;
number += number >>> 8;
number += number >>> 16;
return number & 0x3f;
}
这其实是jdk中bitCount方法的实现。
2.8. 判断一个数是不是2的幂
如果一个整数是2的幂(如果是负数的话,说他的绝对值),那么它的二进制表示中只有1个位是1,我们只需要把它最右边的1置0,然后判断是否为0即可,代码如下:
public boolean isPowerOf2(int number) {
if (number == 0) return false;
number = Math.abs(number);
return (number & (number - 1)) == 0;
}
2.9. 取一个数二进制表示中最右边的1表示的数
意思就是,例如12的二进制表示为0000 1100
,取出最右边的1代表的数,0000 0100
,即4,代码如下:
public int getRightOneBit(int number) {
return number & ((~number) + 1);
}
number取反+1,相当于把number的最右边的1及其右边的0(如果存在的话)保留下来,最后边的1左边的位全部取反了,然后相与,就把最后边的1留下来了。
(~number) + 1
其实就是-number,可以简单记为number&-number
2.10. 取一个数二进制表示中最左边的1表示的数
这里只针对正整数x,例如12的二进制表示为0000 1100
,取出最左边的1代表的数,0000 1000
,即8,就是找到不超过x的最大的2的幂。例如给个7,返回4,给个9返回8,给个8返回8。
此时我们不知道最左边的1在哪个位置,其实方法就是我们把这个数,最左边的1右边全变成1,例如12,0000 1100
,我们想办法变成0000 1111
,这样我们把这个数+1,然后右移一位即可。如何做到把最左边的1右边全变成1呢?其实我们可以把x和(x>>>1)按位或,这样,最左边的1和次左边的位都变成1,然后右移两位,再次或,然后右移四位,八位,16位,代码如下:
public int getLeftOneBit(int number) {
if ((number & (number - 1)) == 0) return number;
number |= number >>> 1;
number |= number >>> 2;
number |= number >>> 4;
number |= number >>> 8;
number |= number >>> 16;
return (number + 1) >>> 1;
}
2.11. 找出大于等于一个数的最小的2的幂
例如给一个5,大于等于5的最小的2的幂是8,因此返回8,给3,返回4,给8,返回8。
其实,这个数值,就是上一个问题《取一个数二进制表示中最左边的1表示的数》,在最后不要右移一位的值,代码如下:
public int getLeftOneBit(int number) {
if ((number & (number - 1)) == 0) return number;
number |= number >>> 1;
number |= number >>> 2;
number |= number >>> 4;
number |= number >>> 8;
number |= number >>> 16;
return number + 1;//这里不要右移一位
}
2.12. 只出现一次的数字
leetcode第136题,异或有两个性质
- 相同的两个数异或为0
- 0跟任何数异或是自己
因此,我们可以用0,跟数组中的所以数异或一次,相同的两个数必然两两异或变为0,剩下的那个数即为所求,代码如下:
public int singleNumber(int[] nums) {
int ans = 0;
for(int i: nums)ans^=i;
return ans;
}
升级版题目:给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。leetcode260题
假设那两个数是a,b。我们按照第一道题的做法,把所有元素异或一遍,得到的值肯定是a^b的结果(记作res),且不为0,我们如何把这两个数拆分出来呢?
思路这样:**res的结果不为0,则肯定某一位是1,我们不妨取res的最右边的1,那么a和b必然在这一位上一个是0,一个是1,我们把数组中这一位是1的所有数组挑出来再次异或一遍,最后得到的结果肯定就是a和b中的一个,记作ans,再把ans和res异或,得到另一个。**代码如下:
public int[] singleNumber(int[] nums) {
int res = 0;
for (int n : nums) res ^= n;
int masker = res & (~res + 1), ans = 0;
for (int n : nums)
if ((n & masker) != 0) ans ^= n;
return new int[]{ans, res ^ ans};
}
以上内容如有错误,码字仓促,请大家不吝赐教。OK,今天的内容据到这里啦,下期再见,拜拜。