原码是一个数的二进制表示形式,最高位表示符号位,0表示正数,1表示负数。
例如,+3的原码为00000011,-3的原码为10000011。
反码是对原码取反的结果,除了符号位不变,其余位都取反。
例如,+3的反码为00000011,-3的反码为11111100。
补码是对反码加1的结果。补码可以解决原码相加减时的溢出问题,并且使得减法运算转化为加法运算。
例如,+3的补码为00000011,-3的补码为11111101。
补码的形成主要是为了解决两个问题:
通过使用补码表示,可以统一处理正数和负数的加减法运算,并且避免了溢出问题,提高了计算机处理负数的效率和准确性。同时,补码还具有唯一的表示形式,不会出现多种表示一个数的情况,避免了歧义。
假设我们使用8位二进制表示形式来表示整数。我们来看一个例子,计算 +3 - 5 的结果。
首先,将+3和-5转换为补码表示:
+3的原码为00000011,反码为00000011,补码为00000011。 -5的原码为10000101,反码为11111010,补码为11111011。
然后,进行减法运算,即将+3的补码与-5的补码相加:
00000011 + 11111011 = 100001010
注意,我们得到了一个9位的结果,超出了原来的8位范围,这是溢出的结果。在这种情况下,我们需要舍弃最高位,只保留低8位作为结果:
00001010
将结果00001010转换为十进制,得到+10。因此,+3 - 5 的结果为+10。
通过使用补码表示,我们可以简单地执行二进制加法运算,并处理溢出的问题。这样,在计算机中对负数进行加减法运算变得更加方便和高效。
与运算的符号是 &,运算规则是:对于每个二进制位,当两个数对应的位都为 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。
移位运算按照移位方向分类可以分成左移和右移,按照是否带符号分类可以分成算术移位
和逻辑移位。
原始:0000 0110 6
右移一次:0000 0011 3 相当于除以2
左移一次:0000 1100 12 相当于乘以2
6*3 =>6*(2+1)=> 6*2+6*1
66*33=>66*(32+1) 66*32+66*1
左移运算的符号是 <<,左移运算时,将全部二进制位向左移动若干位,高位丢弃,低位补 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。
观察上面的例子可以看到,移位运算可以实现乘除操作。由于计算机底层的一切运算都是基于位运算实现的,因此使用移位运算实现乘除法的效率显著高于直接乘除法。
左移运算对应乘法运算:
将一个数左移 k 位,等价于将这个数乘以 2^k。例如,29 左移 2 位的结果是 116,等价于 29×4。
当乘数不是 2 的整数次幂时,可以将乘数拆成若干项 2 的整数次幂之和,例如,a×6 等价于 (a<<2)+(a<<1)。
对于任意整数,乘法运算都可以用左移运算实现,但需要注意溢出的情况,例如在 8 位二进制表示下,29 左移 3 位会出现溢出。
算术右移运算对应除法运算:
将一个数右移 k 位,相当于将这个数除以 2^k。例如,50 右移 2 位的结果是 12,等价于 50/4,结果向下取整。
从程序实现的角度,考虑程序中的整数除法,是否可以说,将一个数(算术)右移 k 位,和将这个数除以 2^k等价?
对于 0 和正数,上述说法是成立的,整数除法是向 0 取整,右移运算是向下取整,也是向 0 取整。但是对于负数,上述说法就不成立了,整数除法是向 0 取整,右移运算是向下取整,两者就不相同了。
例如,(−50)>>2 的结果是 −13,而 (−50)/4 的结果是 −12,两者是不相等的。因此,将一个数(算术)右移 k 位,和将这个数除以 2^k是不等价的。
算法题通常限制测试数据在正数和 0 的情况下,可以放心地使用左移或右移操作。
位运算的性质有很多,此处列举一些常见性质,假设以下出现的变量都是有符号整数。
幂等律:a &a=a,a ∣ a=a(注意异或不满足幂等律);
交换律:a & b=b & a,a ∣ b=b ∣ a,a⊕b=b⊕a;
结合律:(a & b) & c=a & (b & c),(a ∣ b) ∣ c=a ∣ (b ∣ c),(a⊕b)⊕c=a⊕(b⊕c);
分配律:(a & b) ∣ c=(a ∣ c) & (b ∣ c),(a ∣ b) & c=(a & c) ∣ (b & c),(a⊕b) & c=(a & c)⊕(b & c);
德摩根律:∼(a & b)=(∼a) ∣ (∼b),∼(a ∣ b)=(∼a) & (∼b);
取反运算性质:−1=∼0,−a=∼(a−1);
与运算性质:a & 0=0,a & (−1)=a,a & (∼a)=0;
或运算性质:a ∣ 0=a,a ∣ (∼a)=−1;
异或运算性质:a⊕0=a,a⊕a=0;
根据上面的性质,可以得到很多处理技巧,这里列举几个:
a & (a−1) 的结果为将 a 的二进制表示的最后一个 1 变成 0;
(补码)a & (−a)的结果为只保留 a 的二进制表示的最后一个 1,其余的 1 都变成 0。
处理位操作时,还有很多技巧,不要死记硬背,理解其原理对解决相关问题有很大帮助。
下面的示例中,1s和0s分别表示与x等长的一串1和一串0:
而如何获取、设置和更新某个位的数据,也有固定的套路。例如:
1. 获取位
该方法是将数字 1 左移 i 位,得到形如 00010000 的值。然后将这个值与 num 执行 “位与” 操作,从而将 i 位之外的所有位清零。最后我们检查结果是否为零。如果不为零,则说明第 i 位为 1;否则第 i 位为 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);
}
以上是针对位运算中获取、设置、清零和更新位的几种常见操作的示例代码。这些方法在处理位级别的操作上非常有用,可以方便地对数字的某一位进行操作和修改。