机器数
一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器树时带符号的,在计算机中用一个数的最高位存放符号,正数为0,负数为1.
比如:
计算机字节长为8位,下面二进制就是机器数:
十进制的 +3,转换成二进制就是 00000011
十进制的 -3,转换成二进制就是 10000011
真值
因为机器数第一位是符号位,所以机器数的形式值就不等于真正的数值。
所以为了好区别,将带符号位的机器数对应的真正数值称为机器数的真值。
比如:
0000 0001 的真值 = +000 0001 = +1
1000 0001 的真值 = -000 0001 = -1
原码
就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如如果是8位二进制:
[+1] 原码 = 0000 0001
[-1] 原码 = 1000 0001
第一位是符号位,因为第一位是符号位,所以8位二进制的取值范围是:
[1111 1111, 0111 1111],即[-127, 127]
反码
正数的反码是其本身,而负数的反码是在其原码的基础上,符号位不变,其余各个位取反。比如:
[+1] = [0000 0001]原 = [0000 0001]反
[-1] = [1000 0001]原 = [1111 1110]反
可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值,通常要将其转换成原码再计算。
补码
在应用中,因为补码能保持加减运算的统一,因此应用更广,其表示方法是:
[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
对于负数,补码表示方式也是人脑无法直观看出其数值的,通常也需要转换成原码再计算其数值。
看个例子,计算十进制的表达式: 1-1=0,首先看原码的表示:
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的,这也是为何计算机内部不使用原码表示一个数。
为了解决原码做减法的问题就出现了反码,此时计算十进制的表达式为: 1-1=0
1 - 1 = 1 + (-1)
= [0000 0001]原 + [1000 0001]原
= [0000 0001]反 + [1111 1110]反
= [1111 1111]反 = [1000 0000]原
= -0
可以看到用反码计算减法结果的真值部分是正确的,但是"0"的表示有点奇怪,+0和-0是一样的,而且0带符号是没有任何意义,而且要浪费[0000 0000]原和[1000 0000]原两个编码来表示0。于是补码的出现,解决了0的符号以及两个编码的问题:
1-1 = 1 + (-1)
= [0000 0001]原 + [1000 0001]原
= [0000 0001]补 + [1111 1111]补
= [0000 0000]补 = [0000 0000]原
这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了,而且可以用[1000 0000]表示-128:
(-1) + (-127)
= [1000 0001]原 + [1111 1111]原
= [1111 1111]补 + [1000 0001]补
= [1000 0000]补
-1-127的结果应该是-128,我们正好可以用[1000 0000]来表示-128,这样使用补码表示的范围为[-128, 127],这一点也比原码的[-127,127]好。
拓展一下,对于编程中常用到的32位int类型,可以表示范围是: [-2^31, 2^31-1] ,这也是我们在应用中经常见到的定义方式。
对于计算机中的按位运算,包括与操作(AND)、或操作(OR)、异或操作(XOR)等,使用补码表示是最为常见的,因为补码可以统一处理正数和负数,而且在进行数值运算时能够保持一致性。
综上所述,补码在计算机内部的数值表示和运算中具有很多优势,可以统一处理正数和负数、简化逻辑设计,并且处理溢出等情况更为方便。因此,在计算机中的位运算,如与操作,通常都是使用补码表示来进行的。
位运算主要有:与、或、异或、取反、左移和右移。其中左移和右移统称为移位运算,移位运算又分为算数移位和逻辑移位。
与运算符:& ,运算规则是:对于每个二进制位,两个数对应的位都是1时,结果为1,否则结果为0。(都是1为1,否则是0)
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
或运算符:| ,运算规则是:对于每个二进制位,两个数对应的为都是0时,结果为0,否则结果为1。(都是0为0,为否是1)
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
异或运算符:⊕(在代码中用∧ 表示异或),运算规则是:对于每个二进制位,两个数对应的位相同时,结果为 0,否则结果为 1。(相同为0,不同为1)
0 ⊕ 0 = 0
0 ⊕ 1 = 1
1 ⊕ 0 = 1
1 ⊕ 1 = 0
取反运算符:~ ,运算规则是:对一个数的每个二进制位进行取反操作,0变成1,1变成0。(每个位 0变1,1变0)
~0 = 1
~1 = 0
左移运算符: <<,将全部二进制位向左移动若干位,高位丢弃,低位补 0。对于左移运算,算术移位和逻辑移位是相同的。
右移运算符: >>,将全部二进制位向右移动若干位,低位丢弃,高位的补位由算术移位或逻辑移位决定:
原始 0000 0110 6
右移一次:0000 0011 3 相当于除以2
左移一次:0000 1100 12 相当于乘以2
观察上面的例子可以看到,移位运算可以实现乘除操作。由于计算机的底层的一切运算都是基于位运算实现的,因此使用移位运算实现乘除法的效率显著高于直接乘除法的。
左移运算对应乘法运算。将一个数左移 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);
−1=~0
在计算机中,负数的表示通常使用二进制的补码表示法。
-1 = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
0 = [0000 0000]原 = [0000 0000]反 = [0000 0000]补
~0 = [1111 1111]补
在这种特定情况下,-1与~0的二进制表示是相同的。
−a=~(a−1)
假设a的二进制表示是:00000101(即十进制的5)。
考虑等式的右边:~(a - 1)
那么(a - 1)的二进制表示就是00000100(即十进制的4)。
对(a - 1)的每一位按位取反,得到:11111011。
计算等式的左边:-a
首先取a的每一位按位取反,得到:11111010。
然后再加1,得到:11111011,与等式右边的结果相同。
这个等式的成立是基于补码运算和按位取反的性质。
与运算性质:a & 0=0,a & (−1)=a,a & (∼a)=0;
a & (−1)=a
在二进制补码表示中,-1 的所有位都是1,即所有位取反后加1。
-1 = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
无论 a 的某一位是0还是1,在与 -1 进行与操作后,结果的对应位都会保持不变。
a & (~a)=0
对于任意整数 a,按位取反操作 ~a 将 a 的每一位取反(0 变 1,1 变 0)
如果 a 的某一位是 0,那么 ~a 的对应位是 1,所以在 & 运算中结果位为 0。
如果 a 的某一位是 1,那么 ~a 的对应位是 0,所以在 & 运算中结果位也为 0。
或运算性质:a ∣ 0=a,a ∣ (∼a)=−1;
a ∣ (~a)=−1
对于任意整数 a,按位取反操作 ~a 将 a 的每一位取反(0 变 1,1 变 0)
如果 a 的某一位是 0,那么 ~a 的对应位是 1,所以在 | 运算中结果位为 1。
如果 a 的某一位是 1,那么 ~a 的对应位是 0,所以在 | 运算中结果位也为 1。
异或运算性质:a⊕0=a,a⊕a=0;
处理位操作时,还有很多技巧,不要死记硬背,理解其原理对解决相关问题有很大帮助。下面的示例中,1s和0s分别表示与x等长的一串1和一串0:
如何获取、设置和更新某个位的数据,也有固定的套路。例如:
该方法是将1左移 i 位,得到形如00010000的值。接着对这个值与num执行”位与“操作,从而将 i 位之外的所有位清零,最后检查该结果是否为零。不为零说明 i 位为1,否则 i 位为0。代码如下:
func getBit(num int, i int) bool {
return num & (1 << i) != 0
}
setBit先将1左移 i 位,得到形如00010000的值,接着堆这个值和num执行”位或“操作,这样只会改变 i 位的数据。这样除 i 位外的位均为零,故不会影响num的其余位。代码如下:
func setBit(num int, i int) int {
return num | (1 << i)
}
该方法与setBit相反,首先将1左移 i 位获得形如00010000的值,对这个值取反进而得到类似11101111的值,接着对该值和num执行”位与“,故而不会影响到num的其余位,只会清零 i 位。
func clearBit(num int, i int) int {
mark := ~(1 << i)
return num & mark
}
这个方法是将setBit和clearBit合二为一,首先用诸如11101111的值将num的第 i 位清零。接着将待写入值 v 左移 i 位,得到一个 i 位为 v 但其余位都为0的数。最后对之前的结果执行”位或“操作,v为1这num的 i 位更新为1,否则为0:
func updateBit(num int, i int, v int) int {
mark := ~(1 << i)
return (num & mark) | (v << i)
}