计算机中的数在内存中都是以二进制形式进行存储的,用位运算就是直接对整数在内存中的二进制位进行操作,因此其执行效率非常高,在程序中尽量使用位运算进行操作,这会大大提高程序的性能。
位操作符
与&
复制 1 0 0 1 1
1 1 0 0 1
------------------------------
1 0 0 0 1
与运算 两个位都是 1 时,结果才为 1,否则为 0
或 |
复制 1 0 0 1 1
1 1 0 0 1
------------------------------
1 1 0 1 1
或运算 两个位都是 0 时,结果才为 0,否则为 1
异或运算 ^
即无进位相加
复制 1 0 0 1 1
1 1 0 0 1
-----------------------------
0 1 0 1 0
异或运算,两个位相同则为 0,不同则为 1
使用无进位相加理解上述两个性质非常容易
取反运算 ~
复制 1 0 0 1 1
-----------------------------
0 1 1 0 0
取反运算,0 则变为 1,1 则变为 0
左移运算 <<
复制 int a = 8;
a << 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位后:0000 0000 0000 0000 0000 0000 0100 0000
左移运算,向左进行移位操作,高位丢弃,低位补 0
右移运算 >>
复制 unsigned int a = 8;
a >> 3;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位后:0000 0000 0000 0000 0000 0000 0000 0001
右移运算,向右进行移位操作,对无符号数,高位补 0,对于有符号数,高位补符号位
无符号右移运算 >>>
复制 unsigned int a = -1;
a >>> 1;
移位前:0000 0000 0000 0000 0000 0000 0000 1000
移位后:0000 0000 0000 0000 0000 0000 0000 0001
无符号右移运算,向右进行移位操作,高位补 0
负数的表示
计算机32位int值,如果是无符号整型,可以表示的数字范围位0 ~ 2^32-1
,如果是有符号整型,可表示的数字范围为-2^31 ~ 2^31-1
,包含0,他们都共可以表示 2 ^ 32
个数字。
无符号整型将所有32位都作为值存储,而有符号整型则将最高位作为符号位,当符号位为0代表该数字为非负数(可能为0以及正数),如果符号位数字为1,代表该数字为负数,比如在Java中:
复制 print(Integer.MAX_VALUE); // 01111111111111111111111111111111 int最大值 正值
print(Integer.MIN_VALUE); // 10000000000000000000000000000000 int最小值 负值
print(1); // 00000000000000000000000000000001
print(-1); // 11111111111111111111111111111111
负数二进制表示的计算:
复制 假设负数 -1:
最高位也就是符号位为1,也就是 100....00
后面的31位表示数字值,对绝对值使用取反加一计算:
原值 => 100....01
取反 => 111....10
加一 => 111....11 这个值就是 -1 的二进制表示
----------
假设负数位 Integer最小值也就是 -2^31
最高位也就是符号位为1,也就是 100....00
后面的31位表示数字值,对绝对值使用取反加一计算:
原值 => 1100....000 注意,这里溢出一位
取反 => 1011....111
加一 => 1111....111
结果 => 1111....11 溢出的一位直接去除,这个值就是 -2^31的二进制表示
取反加一就是二进制的补码。
为什么负数要使用二进制补码的形式?
参考:https://blog.csdn.net/leonliu06/article/details/78685197
总结:一种巧妙地设计,也是一种规定,为了提高二进制计算的效率
常见的位运算问题
32位int转换为二进制字符串
复制 /**
* 打印数字的二进制表现字符串
*/
public static void print(int num) {
for (int i = 31; i >= 0; i--) {
System.out.print((num & (1 << i)) == 0 ? "0": "1");
}
System.out.println();
}
说明:便利32位int的每一位,将每位的状态打印。比如,第一位与 100....00
相与,可得到该数第一位的状态;第二位与0100...0
相与,可得到第二位的状态,一直到第最后一位,从 31 到 0。
交换数字(异或)
复制 a ^= b;
b ^= a;
a ^= b;
原理:
复制 int a = x;
int b = y;
a ^= b; // a = x ^ y;
b ^= a; // b = y ^ x ^ y; b = x;
a ^= b; // a = x ^ y ^ x; a = y;
注意:
复制 public static void swap(int[] arr, int i, int j) {
arr[i] ^= arr[j];
arr[j] ^= arr[i];
arr[i] ^= arr[j];
}
swap(arr, 0, 1); // 正确
swap(arr, 0, 0); // 错误,arr[i] 与 arr[j] 会都变为0,这是因为arr[i] 与 arr[j] 共用一块内存,如下
// arr[i] ^= arr[j]; // arr[i] = x ^ x; arr[i] = 0;
// arr[j] ^= arr[i]; // arr[j] = 0 ^ 0; arr[j] = 0;
// arr[i] ^= arr[j]; // arr[i] = 0 ^ 0; arr[i] = 0;
判断奇偶数
复制 if(0 == (a & 1)) {
//偶数
}
正负切换(补码)
复制 int reversal(int a) {
return ~a + 1;
}
绝对值
复制 int abs(int a) {
int i = a >> 31; // 获取符号位
return i == 0 ? a : (~a + 1); // 符号位为0=>正数不变返回,符号位为1=>负数取反加一返回
}
高低位交换
复制 34520的二进制表示:
10000110 11011000
将其高8位与低8位进行交换,得到一个新的二进制数:
11011000 10000110
其十进制为55430
复制 unsigned short a = 34520;
a = (a >> 8) | (a << 8);
// 1. a >> 8 得到高8位 00000000 10000110
// 2. a << 8 得到低8位 11011000 00000000
// 3. (a >> 8) | (a << 8) 得到 11011000 10000110
统计二进制中 1 的个数
复制 /**
* 统计二进制数中1的个数
*/
public static int count(int num) {
int count = 0;
for (int i = 31; i >= 0; i--) {
count += (num & (1 << i)) == 0 ? 0 : 1;
}
return count;
}
统计二进制1的个数可以分别获取每个二进制位数,然后再统计其1的个数,此方法效率比较低。这里介绍另外一种高效的方法,同样以 34520 为例,我们计算其 a &= (a-1)
的结果:
第一次:计算前:1000 0110 1101 1000
计算后:1000 0110 1101 0000
第二次:计算前:1000 0110 1101 0000
计算后:1000 0110 1100 0000
第二次:计算前:1000 0110 1100 0000
计算后:1000 0110 1000 0000
我们发现,没计算一次二进制中就少了一个 1,则我们可以通过下面方法去统计:
复制 count = 0
while(a){
a = a & (a - 1);
count++;
}
位运算实现加减乘除
计算机底层都是使用位运算实现加减乘除。
加法
复制 a = 46; => 0010 1110
b = 20; => 0001 0100
// 使用异或获取无进位相加的结果
a ^ b => 0011 1010 => 58
// 由上面可知,进位信息位于第三位
// 可以使用先&获取同为1的位置,再左移获取进位信息
a & b => 0000 0100
a & b << 1 => 0000 1000 => 8
// 加法和就等于 无进位相加和 + 进位
58 + 8 = 66
// 继续使用上述方式计算 58 + 8
a = 58; => 0011 1010
b = 8; => 0000 1000
a ^ b; => 0011 0010 => 50
a & b; => 0000 1000
a & b << 1 => 0001 0000 => 16
// 继续使用上述计算方式计算 50 + 16
a = 50; => 0011 0010
b = 16; => 0001 0000
a ^ b; => 0010 0010; => 34
a & b; => 0001 0000;
a & b << 1; => 0010 0000; => 32
//继续使用上述计算方式计算 34 + 32
a = 34; => 0010 0010;
b = 32; => 0010 0000;
a ^ b; => 0000 0010; => 2
a & b; => 0010 0000;
a & b << 1; => 0100 0000; => 64
// 继续使用上述计算方式计算 64 + 2
a = 64; => 0100 0000;
b = 2; => 0000 0010;
a ^ b; => 0100 0010; => 66
a & b; => 0000 0000;
没有进位,那么此时a ^ b 的结果就是该加法的结果。
java实现:
复制 public static int add(int a, int b) {
int sum = a;
while (b != 0) {
sum = a ^ b;
b = (a & b) << 1;
a = sum;
}
return sum;
}
减法
复制 // 减法就是 a + (-b), 一个数的相反数可以用 ~b + 1来表示
public static int minus(int a, int b) {
return add(a, add(~b, 1));
}
乘法
乘法的实现方式是乘法的竖式表达程序化:
复制 23
x 12
----
46
23
----
276
二进制表现相同:
10111
x 1100
-----------
00000
00000
10111
10111
-----------
100010100 => 276
程序化:
复制 // 乘法
public static int multi(int a, int b) {
int bi;
int result = 0;
int count = 0;
while (count < 32) {
bi = b & (1 << count);
if (bi != 0) {
result = add(result, a << count);
}
count++;
}
return result;
}
// 乘法更简单的实现
// b每次向右侧移动一位,即可取到某位的状态
public static int multi2(int a, int b) {
int res = 0;
while (b != 0) {
if ((b & 1) != 0) {
res = add(res, a);
}
a <<= 1;
b >>>= 1;
}
return res;
}
除法
复制 举例说明
41 / 3
3 * 2 ^ 0 = 3 < 41
3 * 2 ^ 1 = 6 < 41
3 * 2 ^ 2 = 12 < 41
3 * 2 ^ 3 = 24 < 41
3 * 2 ^ 4 = 48 > 41
商=2^3
对余数继续除:
41 - 24 = 17
17 / 3
3 * 2 ^ 0 = 3 < 17
3* 2 ^ 1 = 6 < 17
3 * 2 ^ 2 = 12 < 17
3 * 2 ^ 3 = 24 > 17
商 = 2^3 + 2^2
对余数继续除
17 - 12 = 5
3 * 2 ^ 0 = 3 < 5
3 * 2 ^ 1 = 6 > 5
商 = 2^3 + 2^2 + 2^0
5 - 3 = 2
2 < 3 不继续除
所以,41 / 3 = 2^3 + 2^2 + 2^0 = 13
使用除2的方式是因为2可以使用位运算替代。这种方式实际就是不断的减去被除数,并计算被减的次数。
Java代码实现如下:
控制某一位
假设有二进制数1011 0110
:
控制这个二进制数的最高位变为0:
复制 1011 0110
&0111 1111
0011 0110
控制这个二进制数的最低位变为1:
复制 1011 0110
|0000 0001
1011 0111
java实现:
复制 /**
* 设置二进制数的某一位
* @param num 二进制数
* @param pos 位置
* @param flag true => 1, false => 0
*/
public static int set(int num, int pos, boolean flag) throws IllegalArgumentException{
if (pos < 0 || pos > 31) {
throw new IllegalArgumentException();
}
if (flag) {
num |= (1 << pos);
return num;
}
num &= ~ (1 << pos);
return num;
}
位图的实现
Java:
复制 static class BitMap {
// 1个long类型可以存储64位
// 使用一个long类型数组存储bit位图
private final long[] bits;
// 根据max确定数组的长度
// 如果 max 属于 0 ~ 63 则数组长度为1
// 如果 max 属于 0 ~ 127 则数组长度为2
public BitMap(int max) {
if (max < 0) throw new IllegalArgumentException();
bits = new long[(max + 64) >> 6]; // 等同于 (size + 64) / 64
}
// 将某个位置置为1,位置索引从0开始
public void add(int num) {
// 1. 确定num位置属于数组的第几个整数元素: 0/64=0, 64/64=1, 127/64=1, 128/64=2
// 故元素位置= num / 64 也就是 num >> 6
// 2. 确定num位于已确定的整型元素的第几位: 0%64=0, 1%64=1, 127%64=63, 128%64=0
// 故num位置位于整型元素的第 num%64位。num%64 等同于 num&63:
// num%64等同于只保留二进制的最后6位,前面的位数全部去除,所以 num & 63 = num % 64
// 3. 将指定位置1,使用 |,比如将第2位置1
// 0000 | 0100 = 0100
// 0100 => 1 << (num & 63)
// 故结果如下:
bits[num >> 6] |= (1L << (num & 63));
}
public void delete(int num) {
// 1. 确定num位置属于数组的第几个整数元素: num >> 6
// 2. 确定num位于已确定的整型元素的第几位: num % 64 => num & 63
// 3. 将指定位置0,使用&,比如将第二位置0
// 1111 & 1011 = 1011
// 1011 => ~0100
// 故结果如下
bits[num >> 6] &= ~(1L << (num & 63));
}
public boolean contains(int num) {
// 1010 & 0100 => 0000 说明第二位是0,否则第二位是1
return (bits[num >> 6] & (1L << (num & 63))) != 0;
}
}
面试题
异或
一个数组中有一个数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这个数字?
复制 // 给定数组 [4, 1, 3, 3, 2, 1, 3, 1, 2, 3, 2, 1, 4]
// 其中 1共有4个,2共有3个,3共有4个,4共有2个
// 找到个数为奇数的数字,也就是2
// 使用异或
public static void main(String[] args) {
int[] arr = {4, 1, 3, 3, 2, 1, 3, 1, 2, 3, 2, 1, 4};
int t = 0;
for (int i = 0; i < arr.length; i++) {
t ^= arr[i];
}
System.out.println(t); // 2
}
原理:
满足交换律,所以 4, 1, 3, 3, 2, 1, 3, 1, 2, 3, 2, 1, 4
即为:1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4
相同的数字异或为0, 所以上述数字偶数个的都会被异或为0,奇数个的最中会剩余一个数字
怎么将一个int类型的数,提取出他二进制最右侧的1来?
复制 public static void main(String[] args) {
int N = 200;
System.out.println(Integer.toBinaryString(N)); // 11001000
// 提取最右侧的1,使用公式:N & ((~N) + 1)
int result = N & ((~N) + 1);
System.out.println(Integer.toBinaryString(result)); // 1000
}
推算过程:
复制 原数 .... 0000 0000 1100 1000
取反 .... 1111 1111 0011 0111
加一 .... 1111 1111 0011 1000
相与 .... 0000 0000 0000 1000
一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数?
复制 // 假设两个奇数为 a 和 b
// eor = 将数组中所有元素相互异或
// eor = a ^ b != 0
// 因为eor != 0,故eor 在至少有一个位置上一定有1,所以 a 和 b 在这些位置上的 0 与 1 的状态一定不一致
// 取eor出最右侧的1,与数组的所有元素相与,可以得出该位置为1的所有元素,其中一定含有 a 或者 b
// 将这些元素再次相互异或,可以得到 a 或者 b, 根据 eor = a ^ b; a = eor ^ b; 故而得到 a 和 b
// 代码如下:
// 假设两个奇数为 a 和 b
// eor = 将数组中所有元素相互异或
// eor = a ^ b != 0
// 因为eor != 0,故eor 在至少有一个位置上一定有1,所以 a 和 b 在这些位置上的 0 与 1 的状态一定不一致
// 取eor出最右侧的1,与数组的所有元素相与,可以得出该位置为1的所有元素,其中一定含有 a 或者 b
// 将这些元素再次相互异或,可以得到 a 或者 b, 根据 eor = a ^ b; a = eor ^ b; 故而得到 a 和 b
// 代码如下:
public static void printOddNum2(int[] arr) {
int eor = 0;
for (int i = 0; i < arr.length; i++) {
eor ^= arr[i];
}
int rightOne = eor & (~eor + 1);
int aOrB = 0;
for (int i = 0; i < arr.length; i++) {
if ((arr[i] ^ rightOne) == 0) {
aOrB ^= arr[i];
}
}
System.out.println(aOrB + " " + (eor ^ aOrB));
}
找到数字N的二进制的含有1的数量?
复制 // 以rightOne作为判断条件
public static int bit1Count(int n) {
int count = 0;
int rightOne;
while (true) {
// 找到最右侧的1
rightOne = n & (~n + 1);
if (rightOne == 0) {
return count;
}
count ++;
// 抹掉最右侧的1
n ^= rightOne;
}
}
// 以n数字作为判断条件
public static int bit1Count2(int n) {
int count = 0;
int rightOne;
while (n != 0) {
// 找到最右侧的1
rightOne = n & (~n + 1);
count ++;
n ^= rightOne;
}
return count;
}