位运算作为计算机的核心基础,数据的表示和计算几乎都少不了,在JVM以及很多高性能代码李大量使用,甚至很多算法本身就是基于位运算进行的。许多算法看起来和位运算无关,但是用位运算操作优化一下,性能会提升很多,所以位运算的问题值得好好学习。
学习位运算之前,我们要先明确计算机原码、反码、补码的概念和表示方法,之后介绍位运算相关的问题。
1 数字在计算机中的表示
原码 就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值,比如如果是8为二进制:
[+1]原 = 0000 0001
[-1]原 = 1000 0001
第一位为符号位,所以8为二进制数的取值范围就是:
[1111 1111, 0111 1111], 即[-127 , 127]
反码 的表示方法是:正数的反码是其本身,而负数的反码是在其原码的基础上,符号位不变,其余各位取反。例如:
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值,通常要将其转
成原码再计算。
在应用中,因为 补码 能保持加和减运算的统一,因此应用更广,其表示方法是:
- 正数的补码就是其本身;
- 负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1(即在反码的基础上+1)。
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
2 位运算规则
本节内容应该大部分同学都学过,但是请再认真思考一遍,因为大量算法解决思路都是从这里引申出来的。
位运算主要有:与、或、异或、取反、左移和右移,其中左移和右移统称移位运算,移位运算又分为算术移位和逻辑移位。
2.1 与、或、异或和取反
与运算的符号是 &,运算规则是:对于每个二进制位,当两个数对应的位都为 1 时,结果才为 1,否则结果为 0。
0 & 0=0
0 & 1=0
1 & 0=0
1 & 1=1
或运算的符号是 |,运算规则是:对于每个二进制位,当两个数对应的位都为 0 时结果才为 0,否则结果为 1。
0 ∣ 0=0
0 ∣ 1=1
1 ∣ 0=1
1 ∣ 1=1
异或运算的符号是 ⊕(在代码中用∧ 表示异或),运算规则是:对于每个二进制位当两个数对应的位相同时,结果为 0,否则结果为 1。
0⊕0=0
0⊕1=1
1⊕0=1
1⊕1=0
取反运算的符号是 ∼,运算规则是:对一个数的每个二进制位进行取反操作,0 变
1,1 变成 0。
∼0=1
∼1=0
以下例子显示上述四种位运算符的运算结果,参与运算的数字都采用有符号的 8 位进制表示。
- 46 的二进制表示是 00101110,51 的二进制表示是 00110011。考虑以下位运算结果。
- 46&51的结果是34,对应的二进制表示是00100010。
- 46|51 的结果是63,对应的二进制表示是00111111。
- 46⊕51 的结果是29,对应的二进制表示是00011101。
- ∼46 的结果是−47,对应的二进制表示是11010001。
- ∼51 的结果是 −52,对应的二进制表示是 11001100。
2.2 移位运算
移位运算按照移位方向分类可以分成左移和右移,按照是否带符号分类可以分成算术移位和逻辑移位。原始:0000 0110 6
右移一次:0000 0011 3 相当于除以2
左移一次:0000 1100 12 相当于乘以2
左移运算的符号是 <<,左移运算时,将全部二进制位向左移动若干位,高位丢弃
低位补 0。对于左移运算,算术移位和逻辑移位是相同的。
右移运算的符号是 >>。右移运算时,将全部二进制位向右移动若干位,低位丢弃
高位的补位由算术移位或逻辑移位决定:
- 算术右移时,高位补最高位;
- 逻辑右移时,高位补 0。
以下例子显示移位运算的运算结果,参与运算的数字都采用有符号的 8 位二进制表
示。 - 示例1:29 的二进制表示是 00011101。29左移 2 位的结果是 116,对应的二
进制表示是 01110100;29 左移 3 位的结果是 −24,对应的二进制表示是
11101000。 - 示例2:50的二进制表示是 00110010。50 右移 1 位的结果是 25,对应的二
制表示是 00011001;50 右移 2 位的结果是 12,对应的二进制表示是
00001100。对于 0和正数,算术右移和逻辑右移的结果是相同的。 - 示例3:-50的二进制表示是 11001110(补码)。-50 算术右移 2 位的结果是
−13,对应的二进制表示是 11110011;−50 逻辑右移 2位的结果是 51,对应
的二进制表示是 00110011。
右移运算中的算术移位和逻辑移位是不同的,计算机内部的右移运算采取的是哪一
呢?
- 对于 C/C++ 而言,数据类型包含有符号类型和无符号类型,其中有符号类型
用关键字signed 声明,无符号类型使用关键字 unsigned 声明,两个关键字都
不使用时,默认是有符号类型。对于有符号类型,右移运算为算术右移;对于
符号类型,右移运算为逻辑右移。 - 对于 Java 而言,不存在无符号类型,所有的表示整数的类型都是有符号类型
因此需要区分算术右移和逻辑右移。在Java 中,算术右移的符号是 >>,逻辑
移的符号是 >>>。
2.3 位运算常用技巧
1. 获取(判断第i位是否不为0)
该方法是将1左移i位,得到形如00010000的值。接着堆这个值与num执行“位于”操作,从而将i位之外的所有位清零,最后检查该结果是否为零。不为零说明i为为1,否则为0.代码如下:
boolean getBit(int num, int i){
return ((num & (1<<i)) != 0);
}
2.设置
setBit先将1左移i位,得到形如00010000的值,接着堆这个值和num执行”位或“操作,这样只会改变i位的数据。这样除i位外的位均为零,故不会影响num的其余位。代码如下:
int setBit(int num, int i){
return num|(1<<i);
}
3. 清零
该方法与setBit相反,首先将1左移i位获得形如00010000的值,对这个值取反进而得到类似11101111的值,接着对该值和num执行”位与“,故而不会影响到num的其余位,只会清零i位。
int clearBit(int num, int i){
int mask = ~(1<<i);
return num & mask;
}
4.更新
这个方法是将setBit和clearBit合二为一,首先用诸如11101111的值将num的第i位清零。接着将待写入值v左移i位,得到一个i位为v但其余位都为0的数。最后对之前的结果执行”位或“操作,v为1这num的i位更新为1,否则为0:
int updateBit(int num, int i, int v){
int mask = ~(1 << i);
return (num & mask) | (v << i);
}
上面这几种方法要透彻理解,不要死记硬背,很多棘手问题就能逐步拆解出解决的方法。