目录
本文总结自互联网,对计算机中的编程语言基础知识进行了介绍,主要包括对位的运算,原码、反码、补码的释疑,是对以往的基础知识加强。
目录:
编程语言基础知识位运算1
编程语言基础知识位运算2
再说原码,反码,补码1
再说原码,反码,补码2
先看位运算的“装逼指南”
位算法的效率有多快我就不说,不信你可以去用 10 亿个数据模拟一下,今天给大家讲一讲位运算的一些经典例子。不过,最重要的不是看懂了这些例子就好,而是要在以后多去运用位运算这些技巧,当然,采用位运算,也是可以装逼的,不信,你往下看。我会从最简单的讲起,一道比一道难度递增,不过居然是讲技巧,那么也不会太难,相信你分分钟看懂。
判断奇偶数
判断一个数是基于还是偶数,相信很多人都做过,一般的做法的代码如下
if( n % 2) == 01
// n 是个奇数
}
如果把 n 以二进制的形式展示的话,其实我们只需要判断最后一个二进制位是 1 还是 0 就行了,如果是 1 的话,代表是奇数,如果是 0 则代表是偶数,所以采用位运算的方式的话,代码如下:
if(n & 1 == 1){
// n 是个奇数。
}
有人可能会说,我们写成 n % 2 的形式,编译器也会自动帮我们优化成位运算啊,这个确实,有些编译器确实会自动帮我们优化。但是,我们自己能够采用位运算的形式写出来,当然更好了。别人看到你的代码,我靠,牛逼啊。无形中还能装下逼,是不是。当然,时间效率也快很多,不信你去测试测试。
2、交换两个数
交换两个数相信很多人天天写过,我也相信你每次都会使用一个额外来变量来辅助交换,例如,我们要交换 x 与 y 值,传统代码如下:
int tmp = x;
x = y;
y = tmp;
这样写有问题吗?没问题,通俗易懂,万一哪天有人要为难你,**不允许你使用额外的辅助变量来完成交换呢?**你还别说,有人面试确实被问过,这个时候,位运算大法就来了。代码如下:
x = x ^ y // (1)
y = x ^ y // (2)
x = x ^ y // (3)
我靠,牛逼!三个都是 x ^ y,就莫名交换成功了。在此我解释下吧,我们知道,两个相同的数异或之后结果会等于 0,即 n ^ n = 0。并且任何数与 0 异或等于它本身,即 n ^ 0 = n。所以,解释如下:
把(1)中的 x 带入 (2)中的 x,有
y = x^y = (xy)y = x(yy) = x^0 = x。 x 的值成功赋给了 y。
对于(3),推导如下:
x = x^y = (xy)x = (xx)y = 0^y = y。
这里解释一下,异或运算支持运算的交换律和结合律哦。
以后你要是别人看不懂你的代码,逼格装高点,就可以在代码里面采用这样的公式来交换两个变量的值了,被打了不要找我。
讲这个呢,是想告诉你位运算的强大,让你以后能够更多着去利用位运算去解决一些问题,一时之间学不会也没事,看多了就学会了,不信?继续往下看,下面的这几道题,也是非常常见的,可能你之前也都做过。
3、找出没有重复的数
给你一组整型数据,这些数据中,其中有一个数只出现了一次,其他的数都出现了两次,让你来找出一个数 。
这道题可能很多人会用一个哈希表来存储,每次存储的时候,记录 某个数出现的次数,最后再遍历哈希表,看看哪个数只出现了一次。这种方法的时间复杂度为 O(n),空间复杂度也为 O(n)了。
然而我想告诉你的是,采用位运算来做,绝对高逼格!
我们刚才说过,两个相同的数异或的结果是 0,一个数和 0 异或的结果是它本身,所以我们把这一组整型全部异或一下,例如这组数据是:1, 2, 3, 4, 5, 1, 2, 3, 4。其中 5 只出现了一次,其他都出现了两次,把他们全部异或一下,结果如下:
由于异或支持交换律和结合律,所以:
123451234 = (11)(22)(33)(44)5= 00005 = 5。
也就是说,那些出现了两次的数异或之后会变成0,那个出现一次的数,和 0 异或之后就等于它本身。就问这个解法牛不牛逼?所以代码如下
int find(int[] arr){
int tmp = arr[0];
for(int i = 1;i < arr.length; i++){
tmp = tmp ^ arr[i];
}
return tmp;
}
时间复杂度为 O(n),空间复杂度为 O(1),而且看起来很牛逼。
4、m的n次方
如果让你求解 m 的 n 次方,并且不能使用系统自带的 pow 函数,你会怎么做呢?这还不简单,连续让 n 个 m 相乘就行了,代码如下:
int pow(int n){
int tmp = 1;
for(int i = 1; i <= n; i++) {
tmp = tmp * m;
}
return tmp;
}
不过你要是这样做的话,我只能呵呵,时间复杂度为 O(n) 了,怕是小学生都会!如果让你用位运算来做,你会怎么做呢?
我举个例子吧,例如 n = 13,则 n 的二进制表示为 1101, 那么 m 的 13 次方可以拆解为:
m^1101 = m^0001 * m^0100 * m^1000。
我们可以通过 & 1和 >>1 来逐位读取 1101,为1时将该位代表的乘数累乘到最终结果。直接看代码吧,反而容易理解:
int pow(int n){
int sum = 1;
int tmp = m;
while(n != 0){
if(n & 1 == 1){
sum *= tmp;
}
tmp *= tmp;
n = n >> 1;
}
return sum;
}
时间复杂度近为 O(logn),而且看起来很牛逼。
这里说一下,位运算很多情况下都是很二进制扯上关系的,所以我们要判断是否是否位运算,很多情况下都会把他们拆分成二进制,然后观察特性,或者就是利用与,或,异或的特性来观察,总之,我觉得多看一些例子,加上自己多动手,就比较容易上手了。所以呢,继续往下看,注意,先别看答案,先看看自己会不会做。
5、找出不大于N的最大的2的幂指数
传统的做法就是让 1 不断着乘以 2,代码如下:
int findN(int N){
int sum = 1;
while(true){
if(sum * 2 > N){
return sum;
}
sum = sum * 2;
}
}
这样做的话,时间复杂度是 O(logn),那如果改成位运算,该怎么做呢?我刚才说了,如果要弄成位运算的方式,很多时候我们把某个数拆成二进制,然后看看有哪些发现。这里我举个例子吧。
例如 N = 19,那么转换成二进制就是 00010011(这里为了方便,我采用8位的二进制来表示)。那么我们要找的数就是,把二进制中最左边的 1 保留,后面的 1 全部变为 0。即我们的目标数是 00010000。那么如何获得这个数呢?相应解法如下:
1、找到最左边的 1,然后把它右边的所有 0 变成 1
2、把得到的数值加 1,可以得到 00100000即 00011111 + 1 = 00100000。
3、把 得到的 00100000 向右移动一位,即可得到 00010000,即 00100000 >> 1 = 00010000。
那么问题来了,第一步中把最左边 1 中后面的 0 转化为 1 该怎么弄呢?我先给出代码再解释吧。下面这段代码就可以把最左边 1 中后面的 0 全部转化为 1,
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
就是通过把 n 右移并且做或运算即可得到。我解释下吧,我们假设最左边的 1 处于二进制位中的第 k 位(从左往右数),那么把 n 右移一位之后,那么得到的结果中第 k+1 位也必定为 1,然后把 n 与右移后的结果做或运算,那么得到的结果中第 k 和 第 k + 1 位必定是 1;同样的道理,再次把 n 右移两位,那么得到的结果中第 k+2和第 k+3 位必定是 1,然后再次做或运算,那么就能得到第 k, k+1, k+2, k+3 都是 1,如此往复下去…
最终的代码如下
int findN(int n){
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8 // 整型一般是 32 位,上面我是假设 8 位。
return (n + 1) >> 1;
}
这种做法的时间复杂度近似 O(1),重点是,高逼格。
总结
上面讲了 5 道题,本来想写十道的,发现五道就已经写了好久了,,,,十道的话,怕你们也没耐心写完,而且一道比一道难的那种,,,,。
不过呢,我给出的这些例子中,并不是让你们学会了这些题就 Ok,而且让你们有一个意识:很多时候,位运算是个不错的选择,至少时间效率会快很多,而且高逼格,装逼必备。所以呢,以后可以多尝试去使用位运算哦,以后我会再给大家找些题来讲讲,遇到高逼格的,感觉很不错的,就会拿来供大家学习了。
编程语言基础知识1:位运算与移动
位运算之与&、位或|、位异或^
按位与运算符(&)
按位或运算符(|)
异或运算符(^)
左移运算符(<<)
右移运算符(>>)
按位与运算符(&)
参加运算的两个数据,按二进制位进行与运算。
运算规则:0&0=0;0&1=0;1&0=0;1&1=1
即:两位同时为1,结果才为1,否则为0。
例如:3&5,即 0000 0011& 0000 0101 = 00000001,因此3&5的值得1。
另,负数按补码形式参加按位与运算。
与运算的特殊用途:
(1)清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
(2)取一个数中指定位。
方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行与运算后可以得到X中的指定位。
例如:设X=10101110,取X的低4位,用 X & 0000 1111 = 00001110 即可得到;
还可用来取X的2、4、6位。
按位或运算符(|)
参加运算的两个对象,按二进制位进行或运算。
运算规则:0|0=0;0|1=1;1|0=1;1|1=1;
即:参加运算的两个对象只要有一个为1,其值为1。
例如:3|5,即 00000011 | 0000 0101 = 00000111,因此3|5的值得7。
另,负数按补码形式参加按位或运算。
或运算的特殊作用:
(1)常用来对一个数据的某些位置1。
方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。
例如:将X=10100000的低4位置1,用X | 0000 1111 = 1010 1111即可得到。
异或运算符(^)
参加运算的两个数据,按二进制位进行异或运算。
运算规则:0^0=0;0^1=1;1^0=1;1^1=0;
即:参加运算的两个对象,如果两个相应位为异(值不同),则该位结果为1,否则为0。
异或运算的特殊作用:
(1)使特定位翻转找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或即可。
例:X=10101110,使X低4位翻转,用X ^0000 1111 = 1010 0001即可得到。
(2)与0相异或,保留原值 ,X ^ 00000000 = 1010 1110。
下面重点说一下按位异或,异或其实就是不进位加法,如1+1=0,,0+0=0,1+0=1。
异或的几条性质:
1、交换律
2、结合律(即(a^b)^c == a^(b^c))
3、对于任何数x,都有x^x=0,x^0=x
4、自反性: a^b^b=a^0=a;
异或运算最常见于多项式除法,不过它最重要的性质还是自反性:A XOR B XOR B = A,即对给定的数A,用同样的运算因子(B)作两次异或运算后仍得到A本身。这是一个神奇的性质,利用这个性质,可以获得许多有趣的应用。例如,所有的程序教科书都会向初学者指出,要交换两个变量的值,必须要引入一个中间变量。但如果使用异或,就可以节约一个变量的存储空间: 设有A,B两个变量,存储的值分别为a,b,则以下三行表达式将互换他们的值表达式(值):
a=a^b;
b=b^a;
a=a^b;
应用举例1:
1-1000放在含有1001个元素的数组中,只有唯一的一个元素值重复,其它均只出现一次。每个数组元素只能访问一次,设计一个算法,将它找出来;不用辅助存储空间,能否设计一个算法实现?
解法一:显然已经有人提出了一个比较精彩的解法,将所有数加起来,减去1+2+...+1000的和。这个算法已经足够完美了,相信出题者的标准答案也就是这个算法,唯一的问题是,如果数列过大,则可能会导致溢出。
解法二:异或就没有这个问题,并且性能更好。将所有的数全部异或,得到的结果与1^2^3^...^1000的结果进行异或,得到的结果就是重复数。
左移运算符(<<)
将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
例如:a = a<< 2将a的二进制位左移2位,右补0,
左移1位后a = a *2;
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
右移运算符(>>)
将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
操作数每右移一位,相当于该数除以2。
例如:a = a>> 2 将a的二进制位右移2位,
左补0 or 补1得看被移数是正还是负。
小结
3&5首先转换成二进制然后计算。
按位与&
全1为1,其他的为0
按位或!
全0为0,其他为1
异或^
相同为真,不同为假
编程语言基础知识2之位运算与移动
基础知识之位运算
原码,反码,补码:在n位的机器数中,最高位为符号位,该位为0表示为正,为1表示为负;其余n-1位为数值位,各位的值可为零或一。当真值为正时,原码、反码、补码数值位完全相同;当真值为负时,原码的数值位保持原样,反码的数值位是原码数值位的各位取反,补码则是反码的最低位加一。注意符号位不变。
~:位非(补码)
无符号位运算
(1)位运算应用口诀
清零取反要用与,某位置一可用或
若要取反和交换,轻轻松松用异或
(2)位运算符的应用(源操作数s 掩码mask)
按位与--&:
1)、清零特定位 (mask中特定位置0,其它位为1,s=s&mask)
2)、取某数中指定位 (mask中特定位置1,其它位为0,s=s&mask)
按位或--|:
常用来将源操作数某些位置1,其它位不变(mask中特定位置1,其它位为0 s=s|mask)。按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。当参与运算的是负数时,参与两个数均以补码出现。规则如下:
1|1=1
1|0=1
0|1=1
0|0=0
位异或--^:
使特定位的值取反(mask中特定位置1,其它位为0 s=s^mask)。
异或运算的一些特性
异或,英文为exclusive OR,缩写成xor。这是一个数学运算符,应用于逻辑运算。异或的数学符号为⊕,计算机符号为xor。其运算法则为:
a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
异或也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。
异或略称为XOR、EX-OR
程序中有三种演算子:XOR、⊕
使用方法如下
z = x ⊕ y
z = x xor y
一个数和自己做异或的结果是0。从异或的真值表可以看出,不管是0还是1,和0做异或保持原值不变,和1做异或得到原值的相反值。例如:
unsigned int a, b, mask = 1U << 6;
a = 0x12345678;
b = a ^ mask; /* flip the 6th bit */
如果a1^a2^a3……^an之中1的个数为奇数个,否则为偶数个。这条性质可用于奇偶校验(Parity Check)。
x ^ x ^ y == y
例如:交换两个变量的值,不得借助额外的存储空间a = a ^ b; b = b ^ a; a = a ^ b;
实例说明:如果要对一个整数中的某些位进行操作,怎样表示这些位在整数中的位置呢?可以用掩码(Mask)来表示。比如掩码0x0000ff00表示对一个32位整数的8~15位进行操作,举例如下。
1)、取出8~15位
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = (a & mask) >> 8; /* 0x00000056 */
这样也可以达到同样的效果:
b = (a >> 8) & ~(~0U << 8);
2)、将8~15位清0
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a & ~mask; /* 0x12340078 */
3)、将8~15位置1
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a | mask; /* 0x1234ff78 */
基础知识之左移与右移
1、双目运算符
位移位运算符是将数据看成二进制数,对其进行向左或向右移动若干位的运算。位移位运算符分为左移和右移两种,均为双目运算符。
例如:8>>3 (意思是8向右移动3位)第一运算对象是移位对象,第二个运算对象是所移的二进制位数。
2、逻辑移位与算术移位
在嵌入式开发中,移位操作是常用的一种运算。但是在进行移位运算的时候,如果没有考虑到有符号和无符号的移位区别,就很容易掉进陷阱,得不到我们想要的结果。可以看看下面例子,猜猜是什么结果?
signed char i = -125;
i= i >> 2;
cout<< (int)i;
return 0;
编译结果为:-32
为什么有这样的结果?首先介绍两个概念:逻辑移位和算数移位。
逻辑移位,简单理解就是物理上按位进行的左右移动,两头用0进行补充,不关心数值的符号问题。
算术移位,同样也是物理上按位进行的左右移动,两头用0进行补充,但必须确保符号位不改变。
算术移位指令
算术移位指令有:算术左移SAL(ShiftAlgebraic Left)和算术右移SAR(ShiftAlgebraic Right)。算术移位指令的功能描述如下:
(1)算术左移SAL把目的操作数的低位向高位移,空出的低位补0;
(2)算术右移SAR把目的操作数的高位向低位移,空出的高位用最高位(符号位)填补。
逻辑移位指令
此组指令有:逻辑左移SHL(ShiftLogical Left)和逻辑右移SHR(ShiftLogical Right)。逻辑左移/右移指令只有它们的移位方向不同,移位后空出的位都补0。
(1)逻辑左移SHL
(2)逻辑右移SHR
但好奇的是“i<<3”和“i>>3”到底采用的是算术还是逻辑移位呢?
首先了解一下《计算机原理及基础--有符号类型和无符号类型》
通过sizeof查看在计算机中用几个字节存储的,即8在计算机内是以0000 1000形式还是以0000 0000 0000 1000形式!其次了解一下 《原码、反码、补码之间的快速转换和简单运算》。
查看有符号、无符号类型左移或者右移后的数值是多少!
左移
1、当向左边移动3位,采用的什么方式的移动?
结果显示:
ui = 64 无符号类型左移(正数)
sizeof(unsigned int ) = 4
i = 64 有符号类型左移(正数)
sizeof(int ) = 4
fi = -64 有符号类型左移(负数)
分析:
结论:不管是否有无符号类型,也不管值的正负,均采用的是逻辑左移。
右移
1、当向右边移动3位,采用的什么方式的移动???
结果显示:
ui = 1 无符号类型右移(正数)
sizeof(unsigned int ) = 4
i = 1 有符号类型右移(正数)
sizeof(int ) = 4
fi = -1 有符号类型右移(负数)
分析:
结论:说明只要是有符号数,不管值是正还是负,右移时采用的都是算术右移。再加一个实例:
(1)unsigned char x=3;
x<<1是多少?x>>1是多少?
(2)char x=3;
x<<1是多少?x>>1是多少?
(3)char x=-3;
x<<1是多少?x>>1是多少?
3写成二进制数是00000011;-3写成二进制数是(补码)11111101。
程序执行的时候,操作的是数值的编码表示,也就是数值在内存中的二进制表示。比如说,程序取-3的时候,就去取11111101。
(1)对无符号数3来说,x<<1往左移一位,最左边的位移掉了,最右边的移进来的位补零。变成00000110,所以结果是6;x>>1往右边移一位,由于是无符号数,所以逻辑右移,最右边一位移掉,最左边移进来的位补零,变成00000001,所以结果是1。
(2)对于有符号数3来说,x<<1往左移一位,最左边的位移掉了,最右边的移进来的位补零。变成00000110,所以结果是6;x>>1往右边移一位,由于是有符号数,可能发生逻辑右移,也可能发生算术右移,这一点,C标准并没有明确地指定是使用逻辑右移还是算术右移。但大多数的机器都使用算术右移,变成00000001,所以结果还是1。但是请注意,这只是说大多数的机器是这样的,你敢保证自己不会碰到特殊情况吗?
(3)对于有符号数-3来说,x<<1往左移一位,最左边的位移掉了,最右边的移进来的位补零。变成11111010,结果是-6。往右移一位,由于是有符号数,可能发生逻辑右移,也可能发生算术右移。大多数机器使用算术右移,变成11111110,结果是-2。
总结:左移时总是移位和补零。右移时无符号数是移位和补零,此时称为逻辑右移;而有符号数大多数情况下是移位和补最左边的位(也就是补最高有效位),移几位就补几位,此时称为算术右移。
综上所述:
左移时总是移位和补零,无论是有符号类型数据还是无符号类型数据都统称为逻辑左移。
右移时无符号数是移位和补零,此时称为逻辑右移。
右移时而有符号数大多数情况下是移位和补最左边的位(也就是补最高有效位),移几位就补几位,此时称为算术右移。
再说原码,反码,补码1
本节从原码讲起,通过简述原码,反码和补码存在的作用,加深对补码的认识。力争对补码的概念不再局限于:负数的补码等于反码加一。
接触过计算机或电子信息相关课程的同学,应该都或多或少看过这码仨哥。每次都是在课本的最前几页,来上这么一段:什么反码是原码除符号位,按位取反。补码等于反码加一。然后给整得莫名其妙,稀里糊涂地,接着就是翻页,后面的内容也跟三码没多大关系。我原来也是看了好几遍都没看懂,古人云:事不过三。
学C语言的时候,看过一次,不懂?
看《计算机基本组成原理》的时候看过,还是不懂!
到了大三,上《单片微机原理与接口技术》的时候仍旧是不懂。
到了期末,复习的时候和人说这些码呀,自己也不是很清楚呀。然后就一边说怎么求码,一边算。玩着玩着,突然就明白了。放假时好好整理下思路,于是就有了本篇。好了,废话不多说,开始我们的原码,反码,补码之旅。
(一)预备知识
认识二进制,十六进制,会二进制与十进制的相互转化运算。
由计算机的硬件决定,任何存储于计算机中的数据,其本质都是以二进制码存储。
根据冯~诺依曼提出的经典计算机体系结构框架。一台计算机由运算器,控制器,存储器,输入和输出设备组成。其中运算器,只有加法运算器,没有减法运算器(据说一开始是有的,后来由于减法器硬件开销太大,被废了)。所以计算机中的没法直接做减法的,它的减法是通过加法来实现的。你也许会说,现实世界中所有的减法也可以当成加法的,减去一个数,可以看作加上这个数的相反数。当然没错,但是前提是要先有负数的概念。这就为什么不得不引入一个该死的符号位。
而且从硬件的角度上看,只有正数加负数才算减法。
正数与正数相加,负数与负数相加,其实都可以通过加法器直接相加。
原码,反码,补码的产生过程,就是为了解决计算机做减法和引入符号位(正号和负号)的问题。
本文可能比较长,没必要一下子读完。原码,反码,补码,按章读。重点在于讲补码,到了补码可能有些绕,建议带着笔,写出二进制数一起算。表达可能不够清楚严谨,望见谅。
(二)原码
***原码***是最简单的机器数表示法。用最高位表示符号位,1表示负号,0表示正号,其他位存放该数的二进制的绝对值。
若以带符号位的四位二进值数为例
1010:最高位为1,表示这是一个负数,其他三位为010
即(0*2^2)+(1*2^1)+(0*2^0)=2('^'表示幂运算符)
所以1010表示十进制数(-2)。
下图给出部份正负数数的二进制原码表示法
OK,原码表示法很简单有没有,虽然出现了+0和-0,但是直观易懂。开始运算。
0001+0010=0011(1+2=3)OK
0000+1000=1000(+0+(-0)=-0) 额,问题不大
0001+1001=1010(1+(-1)=-2)
噢,1+(-1)=-2,这仿佛是在逗我呢。
于是我们可以看到其实正数之间的加法通常是不会出错的,因为它就是一个很简单的二进制加法。
而正数与负数相加,或负数与负数相加,就要引起莫名其妙的结果,这都是该死的符号位引起的。0分为+0和-0也是因他而起。
所以原码,虽然直观易懂,易于正值转换。但用来实现加减法的话,运算规则总归是太复杂。于是反码来了。
(三)反码
我们知道,原码最大的问题就在于一个数加上他的相反数不等于零。
例如:0001+1001=1010 (1+(-1)=-2) 0010+1010=1100 (2+(-2)=-4)
于是反码的设计思想就是冲着解决这一点,既然一个负数是一个正数的相反数,那我们干脆用一个正数按位取反来表示负数试试。
***反码***正数的反码还是等于原码
负数的反码就是他的原码除符号位外,按位取反。
若以带符号位的四位二进制数为例:
3是正数,反码与原码相同,则可以表示为0011
-3的原码是1011,符号位保持不变,低三位(011)按位取反得(100)
所以-3的反码为1100
下图给出部分正负数的二进制数反码表示法
对着上图,我们再试着用反码的方式解决一下原码的问题
0001+1110=1111 (1+(-1)=-0)
互为相反数相加等于0,解决。虽然是得到的结果是1111也就是-0
好,我们再试着做一下两个负数相加
1110(-1)+1101(-2)=1011(-4)
噢,好像又出现了新问题
(-1)+(-2)=(-4)?
不过好像问题不大,因为1011(是-4的反码,但是从原码来看,他其实是-3。巧合吗?)
我们再看个例子吧
1110(-1)+1100(-3)=1010(-5)
确实是巧合,看来相反数问题是解决了,但是却让两个负数相加的出错了。
但是实际上,两个负数相加出错其实问题不大。我们回头想想我们的目的是什么?是解决做减法的问题,把减法当成加法来算。
两个正数相加和两个负数相加,其实都是一个加法问题,只是有无符号位罢了。而正数+负数才是真正的减法问题。
也就是说只要正数+负数不会出错,那么就没问题了。负数加负数出错没关系的,负数的本质就是正数加上一个符号位而已。
在原码表示法中两个负数相加,其实在不溢出的情况下结果就只有符号位出错而已(1001+1010=0011)
反码的负数相加出错,其实问题不大。我们只需要加实现两个负数加法时,将两个负数反码包括符号位全部按位取反相加,然后再给他的**符号位强行置'1'**就可以了。
所以反码表示法其实已经解决了减法的问题,他不仅不会像原码那样出现两个相反数相加不为零的情况,而且对于任意的一个正数加负数,如:
0001(1)+1101(-2)=1110(-1) 计算结果是正确的。所以反码与原码比较,最大的优点,就在于解决了减法的问题。
但是我们还是不满足为什么 0001+1110=1111 (1+(-1)=-0) 为什么是-0呢
而且虽然说两个负数相加问题不大,但是问题不大,也是问题呀。好吧,接下来就介绍补码。
(四)补码
***补码***正数的补码等于他的原码
负数的补码等于反码+1(这只是一种算补码的方式,多数书对于补码就是这句话)。
在《计算机组成原理中》,补码的另外一种算法是
负数的补码等于他的原码自低位向高位,尾数的第一个1及其右边的0保持不变,左边的各位按位取反,符号位不变。
OK,补码就讲完了,再见!
还是莫名其妙有没有,为什么补码等于反码加1,为什么自低位向高位取反…?
其实上面那两段话,都只是补码的求法,而不是补码的定义。很多人以为求补码就要先求反码,其实并不是。
那些鸡贼的计算机学家,并不会心血来潮的把反码+1就定义为补码。只不过是补码正好就等于反码加1罢了。
所以,忘记那些书上那句负数的补码等于它的反码+1。就这句话把我们带入了理解的误区。
这就是后来我明白为什么我看的那本《计算机组成原理》,要特意先讲补码,再讲反码。
然后说负数的补码等于他的原码自低位向高位,尾数的第一个1及其右边的0保持不变,左边的各位按位取反,符号位不变。
但是上面这句话,同样不是补码的定义,它只是补码的另外一种求法。它的存在,告诉我们忘记那句该死的'反码+1'它并不是必须的。
如果你有兴趣了解,补码的严格说法,我建议你可以看一下《计算机组成原理》。它会用'模'和'同余'的概念,严谨地解释补码。
接下来我只想聊聊补码的思想。
(五)补码的思想
补码的思想,第一次见可能会觉得很绕,但是如果你肯停下来仔细想想,绝对会觉得非常美妙。
补码的思想其实就来自于生活,只是我们没注意到而已。时钟,经纬度,《易经》里的八卦。
补码的思想其实就类似于生活中的时钟
好吧,我其实不想用类似,好像这种词,因为类比的,终究不是事物本身。而且不严谨会让我怀疑我不是工科僧,说得好像我严谨过似的
如果说现在时针现在停在10点钟,那么什么时候时针会停在八点钟呢?
简单,过去隔两个小时的时候,是八点钟。未来过十个小时的时候也是八点钟
也就是说时间正拨10小时,或是倒拨2小时都是八点钟。
也就是10-2=8,而且 10+10=8(10+10=10+2+8=12+8=8)
这个时候满12说明时针在走第二圈了,又走了8小时,所以时针正好又停在八点钟。
所以12在时钟运算中,称之为模,超过了12就会重新从1开始算了。
也就是说, 10-2和10+10从另一个角度来看是等效的,它都使时针指向了八点钟。
既然是等效的,那在时钟运算中,减去一个数,其实就相当于加上另外一个数(这个数与减数相加正好等于12,也称为同余数)
这就是补码所谓模运算思想的生活例子
在这里再次强调原码,反码,补码的引入是为了解决做减法的问题。在原码,反码表示法中,我们把减法化为加法的思维是减去一个数,等于加上一个数的相反数,结果发现引入了符号位,却因为符号位造成了各种意向不到的问题。
但是从上面的例子中,我们可以看到其实减去一个数,对于数值有限制,有溢出的运算(模运算)来说,其实也相当于加上这个数的同余数。
也就是说,我们不引入负数的概念,就可以把减法当成加法来算。所以接下来我们聊4位二进制数的运算,也不必急于引入符号位。因为补码的思想,把减法当成加法时并不是必须要引入符号位的。
而且我们可以通过下面的例子,也许能回答另一个问题,为什么负数的符号位是1,而不是正数的符号位是1。
(六)补码实例
好吧,接下来我们就做一做四位二进制数的减法吧(先不引入符号位)
0110(6)-0010(2)[6-2=4,但是由于计算机中没有减法器,我们没法算]
这个时候,我们想想时钟运算中,减去一个数,是可以等同于加上另外一个正数(同余数)
那么这个数是什么呢?从时钟运算中我们可以看出这个数与减数相加正好等于模。
那么四位二进制数的模是多少呢?也就是说四位二进制数最大容量是多少?其实就是2^4=16=10000B
那么2的同余数,就等于10000-0010=1110(14)
既然如此
0110(6)-0010(2)=0110(6)+1110(14)=10100(20=16+4)
OK,我们看到按照这种算法得出的结果是10100,但是对于四位二进制数,最大只能存放4位(硬件决定了),如果我们低四位,正好是0100(4),正好是我们想要的结果,至于最高位的1,计算机会把他放入psw寄存器进位位中。8位机则会放在cy中,x86会放在cf中(这个不作讨论)。
这个时候再想想在四位二进制数中,减去2,就相当于加上它的同余数14(至于它们为什么同余,还是建议看《计算机组成原理》)
但是减去2,从另外一个角度来说,也是加上(-2)。即加上(-2)和加上14其实得到的二进制结果除了进位位,结果是一样的。
如果我们把1110(14)的最高位看作符号位后就是(-2)的补码,这可能也是为什么负数的符号位是'1'而不是'0',
而且在有符号位的四位二进制数中,能表示的只有'-8~7',而无符号位数(14)的作用和有符号数(-2)的作用效果其实是一样的。
那正数的补码呢?加上一个正数,加法器就直接可以实现。所以它的补码就还是它本身。
下图给出带符号位四位二进制的补码表示法
到这里,我们发现原码,反码的问题,补码基本解决了。
在补码中也不存在负零了,因为1000表示-8
这是因为根据上面的补码图,做减法时,0001(1)+1111(-1)=0000
我们再也不需要一个1000来表示负0了,就把它规定为-8
负数与负数相加的问题也解决了1111(-1)+1110(-2)=1101(-3)
可能说得有点绕,但是实在是没办法。其实我觉得补码还可以这样画。
很优美有没有,如果你想想地理课本,0不就相当于本初子午线,-8不就是180°,而正数相当于西经,负数相当于东经。
(七)为何这样求补码
然后我们再来看看为什么负数的补码的求法为什么是反码+1。
因为负数的反码加上这个负数的绝对值正好等于1111,再加1,就是1000,也就是四位二进数的模。
而负数的补码是它的绝对值的同余数,可以通过模减去负数的绝对值,得到他的补码。
所以负数的补码就是它的补码+1。
有点绕吧,只能说很难算清楚,你们还是自己算算吧。还有上面我提到的另外一种算法。
接下来,我要说一下我自己算补码的小技巧。
看上面那个图。
如果我们把-8当成负数的原点。那么-5的补码是多少呢?
-5=-8+3
-5的补码就是-8的补码加3
1000(-8)+0011(3)=1011(-5)
所以完全可以口算出-5的补码是1011
当然,也可以记住-1的补码是1111口算减法得出
对于八位加法器的话,可以把-128当补码原点。十六位可以把-32768当补码原点。
是的,128是256(八位二进制数的模)的一半,32768是65536(十六位二进数的模)的一半
也很方便有没有,而且简单的是
补码原点总是最高位是'1',其他位是'0'
所以做加法总是简单得可以口算。
OK,原码,反码,补码之旅就到这里结束。补码第一次看总会觉得很绕,想言简意赅,就怕哪里遗漏了。讲得细致,又不免连自己都觉得啰里啰嗦。谢观!
再说原码,反码,补码2
其实计算机的设计也是一门艺术的博弈,转自编码珠玑聊的话题:计算机补码的运算设计的精妙绝伦之处。
一、逻辑电路是如何计算加法的
1938年,香农(这个人不用说了吧,计算机行业的人没人没听过他吧)在麻省理工学院发表了那篇题为《继电器和开关电路的符号分析》(A Symbolic Analysis of Relay and Switching Circuits)的著名硕士论文,这是一篇具有划时代意义的论文,在文中清晰地阐述:电子工程师可以运用布尔代数的所有工具去设计开关电路。也就是说逻辑运算居然可以用电路来进行实现,随后人们根据这一理论设计出了各种逻辑门(Logic Gate)来进行数据运算,后期的电子计算机的运算原理都是基于这一理论进行实现的,比如人们根据继电器或者晶体管的特性,设计了异或门(关于异或运算的本质请参考 如何通俗理解异或运算 ):
当开关 A 闭合,线圈产生磁性将开关 M 吸合,接通灯泡的回路,灯泡就会亮,这是一个最简单的逻辑回路。你能想象人类发明CPU甚至所有的存储设备其实就是这一堆堆开关组合成的吗,虽然现代CPU用的是晶体管(速度更快、体积更小),但是原理都是一样的。比如苹果最新发布的M2芯片上面集成了200亿个晶体管,翻译成人话就是上面放了200亿个开关。
正如《道德经》中所云:一生二,二生三,三生万物。
后来人们根据上面那个电路进行简单改造,无非就是开关的常开变常闭,或者常闭变常开等等,发明出了各种不同的逻辑门,可以实现更多的逻辑回路,比如与门(AND)、与非门(NAND)、或门(OR)、或非门(NOR)、异或门(XOR)等等。
比如下面这个与门就是连个开关A与B必须同时闭合灯泡才能亮:
这样的电路人们没想到居然会与二进制的加法存在着某些联系,比如二进制1+1=10的进位是1,而这个与门电路双开关必须同时闭合才会亮,如果闭合代表1,断开代表2,那么逻辑关系就是1 AND 1 = 1.
有一天人们惊奇地发现,一个异或门并联一个与门居然能做简单的二进制位的加法运算,给它命名叫半加器。之所以叫半加器,是因为它还没有办法将进位的输出纳入下一位的运算,比如 1+1=10,等号右边的进位暂时还不能纳入下一位的运算。
把这一堆符号合成一个整体:
半加器
后来人们改进了这个电路,用两个半加器再加一个或门,组成一个全加器,这次就厉害了,全加器弥补了半加器不能计算让进位参与运算的缺点,可以将前一位的进位纳入本位进行一块计算,所以全加器输入端有三个输入:
把上面这一堆符号合成一个整体:
全加器
多个全加器组合在一块就能计算多位的二进制加法,下面这组加法器就能计算四位二进制的加法:
通过这组加法器的组合,就能计算十进制的 5+3=8 运算,很难想象,这样的运算居然是通过几个开关实现的!实际上这正是现代计算机进行加法计算的原理。
到这里,已经能够通过设计的逻辑电路来计算加法了,但是还有个重要的问题:减法如何计算呢?因为计算减法涉及到借位这种繁琐的操作,而上面设计的电路只能进位,难道还要为减法设计特定的逻辑电路吗,答案肯定是否定的,那样的电路就会非常复杂,需要考虑的是如何通过现有的逻辑电路,也就是如何通过加法来计算减法呢?
这个问题特别有意思,有人会说了,减去一个数等于加上这个数的负数,比如 5-3=2 这个式子,可问题是这样的说法实际上还是在计算减法,按照目前设计的开关电路是实现不了的,那怎么办呢?
想象一下上小学的时候,刚开始学习三位数的减法的时候,不喜欢一些带有借位的减法,比如 :
213-147=66
这个算式计算起来很不舒服,首先从个位,3小于7,所以要从十位进位,而十位数借位后还小于4,还要从百位借位。
这里用一个技巧,先用 999 减去减数 147,显然这个算式不会产生借位:
999-147=852
这个 852 称为9的补数,用这个结果与被减数213相加
852+213=1065
最后将结果加1,然后再减去1000:
1065+1-1000=66
居然得到了想要的答案,而且没用到借位。
为什么这个间接的运算会正确呢?这是因为的原题目可以化成下面的运算:
213-147+1000-1000
看到了吧,实际上是加了1000最后又给减去了,再把上式组合一下:
213+(999-147)+1-1000
其实计算结果是一样的,而且避免了借位的运算。
到这里,你可能会有疑惑:可这个式子还用到了减法啊,而且是两次,难道计算机在计算的时候还会有技巧跳过这个减法吗?
在这里,神奇的事情发生了,由于计算机采用的是二进制,第一个减法也就是求补数是从一串1的数字中减去的,而二进制求补的运算不像十进制那样,前者根本不需要做减法,而是将原来二进制中的数字1变为0,0变为1即可(这与直接计算减法结果是一样的,但是这个技巧对计算机来说就省下了做减法的运算),这个求相反数可以称为反码,可以通过逻辑电路中的反向器来实现,第二个减法在二进制中减的是最高位,而这个对计算机来说只需要通过一个逻辑门电路来限制最高位输出即可实现。
下面来看一下使用二进制计算这一过程有多奇妙
第一步,求补运算:
第二步,将结果加上被减数 213:
第三步,将第二步的结果加 1:
第四步,将第三步的结果最高位取反,相当于减去了256:
这样就最终得出了想要的结果:66,整个过程虽然采用了两次减法,但是在二进制看来,根本没有使用减法。
二、为什么采用补码来存储整数
但是,上面这个电路还有局限性,它只能计算被减数大于减数的运算,而且不能表示负数,想要的结果是使用现有的电路,让它能够计算加法、减法、还有负数,换句话说,让所有的运算都按照加法来实现,该如何实现呢?
这时候,补码运算就登场了。
首先,计算机为了区分整数与负数,规定了符号位,规定最高位为「符号位」,0代表正数,1代表负数,剩下的才是「数字位」。例如对于两个字节 short 类型数字 1 在计算机内部是这样表示的:
而整数 -1 的表示方法是这样的,只是符号位变为了 1:
但这样做是有代价的,意味着数据位的表示实际上是少了一位,导致原本能表示的数字没那么大了。例如单字节原本能表示 0 ~ 255 之间的数字,但是因为符号位占据了 1 位,实际表示数据的位数变为了 7 位,最大只能表示 127.
这时候引出反码还有补码这个概念:正数的反码补码都是其原码,而复数的反码比较特殊,符号位不变,数据位取反就是反码,反码加 1 就是补码:
计算机内部所有的运算都采用补码的形式,那么为什么要这样呢?
先来看如果采用原码的形式进行计算,假设要计算 1 - 3,实际上就是1+(-3):
这样得出的结果竟然是 1 + (-3) = -4,结果显然是不正确的
那么如果采用反码进行计算,会怎样呢?
这样得出的结果就是正确的,与预期的一样,但是如果计算 3-1 会怎么样呢,再试试:
最后居然得出 3+(-1)=1 的结果,这说明采用反码运算,小数减大数没问题,但是大数减小数结果就出了问题,直觉告诉我们,结果差了1.
随后人们想出了补码这种神奇般的操作,来看一下它的结果是怎样的:
这样计算的结果就与期待的一样,是正确的。为什么补码运算会正确呢,仔细分析一下:
当大数减去小数的时候,结果一定是正数。而之前采用的反码运算,结果总是少了1,如果采用补码来计算的话,负数从反码转为补码要加上1,在计算出结果后,因为正数的补码与反码相同,所以不用再减去,所以刚好相当于把结果加了1。
当小数减去大数的时候,结果一定是负数。如果采用补码运算,负数从反码转化为补码要加上1,而恰恰,结果是负数,这个负数从补码转为原码又要减去1,刚好抵消,结果不受影响。
补码的发明,彻底简化了硬件电路,不必为减法设计额外的电路,仅仅通过加法电路就能计算减法,真是太神奇了。