1.数字在计算机中的表示
-
机器数:一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是自带符号的,在计算机用一个数的最高位存放符号,整数为0,负数为1。比如,十进制中的数+3,计算机字长8位,转换成二进制就是00000011.如果是-3.就是10000011。两者都是机器数。
-
真值:因为机器数的第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数10000011,其实最高位1代表负,其真正数值是-3,而不是形式数值131(10000011转换成10进制等于131)。所以将带符号位的机器数对应的的真正数值称为机器数的真值。(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
对于负数,补码也需要转换成原码在计算其数值
拓展为何会有原码、反码和补码?
[+1] = [0000 0001]原 = [0000 0001]反 = [0000 0001]补
[-1] = [1000 0001]原 = [1111 1110]反 = [1111 1111]补
我们都知道计算机中只有加法,我们看个例子,计算十进制的表达式:1-1=0,看原码表示:
1- 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [1000 0010]原 = -2
结果不正确
用反码计算:
1- 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原
= [0000 0001]反 + [1111 1110]反
=[1111 1111]反 = [1000 0000] 原 =-0
有点奇怪,0带符号没有意义
用补码计算:
1- 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原
= [0000 0001]补 + [1111 1111]补
=[0000 0000] 补+[0000 0000]原 = 0
负0就不存在了,可以用[1000 0000] 表示-128。补码的表示范围就是[-128 - 127]
-
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,
2.2 移位运算
-
移位运算分为左移和右移,按照是否带符号可以分成算术运算和逻辑移位。
原始:0000 0110 6
右移一次:0000 0011 3 相当于除以2
左移一次:0000 1100 12 相当于乘于2
-
左移运算的符号是<< ,左移运算时,将全部运算时,将全部二进制位向左移动若干位,高位丢弃,低位补0。对于左移运算,算术移位和逻辑移位是相同的。
-
右移运算的符号是>>,右移运算时,将全部的二进制位向右移动若干位,低位丢弃,高位的补位由算术移位或逻辑移位决定:
-
算术移位时,高位补最高位,(负数最高位补1,正数补0)
-
逻辑右移时,高位补0
在计算机中,对于0和正数,算术移位和逻辑移位结果是相同的。负数的结果时不同的。
对于C++,数据类型包含有符号和无符号类型。有符号类型右移为算术右移,无符号类型右移运算为逻辑右移。
对于Java,不存在无符号类型,算术右移是>>,逻辑右移是>>>
-
2.3 移位运算与乘除法的关系
-
左移运算对应乘法关系。低位补0,将一个数左移k位,相当于这个数乘于2^k.
-
右移运算对应除法关系,将一个数右移k位,相当于这个数除以2^k.
需要注意的是:
-
左移无需考虑太多只需低位补0即可。
-
对于负数和正数的右移对应的结果是不一致的,分为算术右移和逻辑右移。算法在出题的时候考虑到这一点,大部分会将数据限制在正数和0的情况 ,因此可以放心的左移或右移。
-
2.4 位运算技巧
位运算的性质有很多,有一些运算公式,
-
幂等律: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 & b) | ( b& c) ,异或同理
-
德摩根律:~(a & b)= ( ~a) | (~ b), ~(a | b) = (~a) & (~b)
-
取反运算性质:-1 = ~0, -a = ~( a - 1)
-
与运算性质:a & 0 = a, 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都变成0.
如何获取、设置、和更新某个位的数据,也有固定的套路。
-
获取
该方法是将1左移i位,得到形如00010000的值,接着对这个值与num执行“位与”操作,从而将i位之外的所有位清零,最后检查该结果是否为零。不为零i位为1,否则i位位0。代码如下:
boolean getBit(int num, int i) {
return ((num & (1 << i))) != 0;
}
-
设置(将某一位置设置为1)
setBit先将1右移i位,得到形如00010000的值,接着对这个值和num执行“位或”操作,这样只会改变i位的数据。除i位外的位均为零,故不会影响num的其余位。代码如下:
int setBIt(int num, int i) {
return num | (i << i);
}
-
清零(将某一位置设置为0)
该方法与setBit方法相反,首先将1左移i位获得形如00010000的值,对这个值取反的到类似11101111的值,接着对该值和num执行“位与”,古不会影响到num的其余位,只会清零i位。代码如下:
int clearBit(int num, int i) {
int mask = ~(1 << i); return num & mask;
}
-
更新
这个方法是将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);
}