同余基本理论
整除:若整数 b 除以整数 a 的商为整数,且余数为 0,则称 a 整除 b,记作 a∣b > 同余:给定一个整数 m,若 m 除整数 a,b 的余数相等,就称 a 与 b 对模 m 同余,记作 a≡b(modm)
下面证明一个 a≡b(modm) 的充要条件
a≡b(modm) 的充要条件为 m∣(a−b)
设 a=q1m+r1,b=q2m+r2,0⩽r1,r2<m
若 a≡b(modm),则 r1=r2,故 a−b=(q1−q2)m,也即有 m∣(a−b)
若 m∣(a−b),则可设 a−b=km(k∈Z),即 (q1−q2)m+r1−r2=km,则 r1=r2,即 a≡b(modm)
从钟表引入
假设钟表显示现在是 10 点,如果我们想让指针指向 6,我们有两种调法
- 要么让指针往回拨 4 个刻度,即 10−4=6
- 要么将指针往前拨 8 个刻度,即 10+8=18→6
为什么要考虑余数?
在十二小时制中,设 n 表示 n 时,那么
- 若 0⩽n⩽12 时,可以直接得到当前的时间就是 n 时
- 若 12<n⩽24,想获取其在十二小时制中对应的时间,我们会用 n 减去 12,
- 若 24<n⩽36,我们则会用 n 减去 2×12
由此可以看出,无论 n 为多少,想获取其在十二小时制对应的时间,则会用 n 减去若干倍的 12,这其实就是一个用 n 模 12 取余的过程(注意这里的若干倍可以表明我们并不关注商是多少)
在上面钟表的例子中,我们想让指针指向 6,采用减法的思想是我们比较容易想到的,因为这样可以直接计算 10−4=6,而如果我们一定要采用第二种方法呢
为什么减4和加8等效?
上面我们讨论到,我们只需要关注余数,也就是说,我们只需要关注 10−4 和 10+8 的余数
一旦注意到这点,我们就不难想到这样一个同余式
(10−4)≡(10+8)(mod12)
在这里,我们称 8 是 −4 在模 12 系统下的补(不难注意到 4 也是 −8 在模 12 系统下的补)
因此,在模 m 的系统下,对于任意的 K,只要满足 A 与 B 互补,则有 (K+A)≡(K+B)(modm)
如何找到对应的补数?
在上面的模 12 系统中,我们很容易找到 −4 的补数,甚至其他数的补数
那么,在任意的一个模 m 的系统中,如何快速的找到一个数的补数呢,还是回到下面这个式子
(K+A)≡(K+B)(modm)
根据前文介绍的定理,这个同余式要成立,当且仅当 m∣[(K+A)−(K+B)] 成立,也就是
m∣(A−B)
写成等式的形式,即 A−B=km(k∈Z),也就是 A=km+B,可以看出 A 的补数可以有无数多个,这里我们取 k=1,那么
A=m+B
为了符号的统一,我们令 A,B>0,且 A 就是我们要的 −B 的补数,那么就得到了
A=m−B
这样,我们就能很容易得到 −4 的补数就是 12−4=8
补码的数学原理
在开始补码之前,我们首先要知道为什么计算机需要补码
以八位二进制数为例,我们依次将最低位到最高位记作第 0 位,第 1 位,第 2 位,…,第 7 位
一般来说,第 7 位是符号位,且正数第 7 位为 0,负数的第 7 位为 1,如
(3)10=(00000011)2,(−5)10=(10000101)2
我们把这种二进制表示法称为数的原码,记作
[3]原=00000011,[−5]原=10000101
接下来我们用原码来计算 3−5,首先我们要知道的是,计算机并不能处理减法,因此这里实际的运算是 3+(−5),如下
00000011+1000010110001000
然而 10001000 对应的十进制数是 −8,而非正确结果 −2
经过尝试,原码只有在计算正数与正数的和时才能完全正确,因此就有了补码
一个 n 位的二进制数其实是一个模 2n 的系统
我们先考虑正数,去掉一个符号位,一个正数还有七位,因此八位二进制数能表达的最大的正数是 27−1=127
那么,从 00000000 开始, 如果将这个数不断加 1,就得到 01111111(即 127),继续加 1,直至 11111111,这时再加 1,就得到了 100000000,而这是一个八位的二进制整数,因此这个数实际上应该表示为 00000000,也就回到了 0
就像时钟一样,从 1 开始走过 12 格后,又回到了 1,因此(十二小时制)时间就是一个模 12 的系统
同样的,一个 n 位的二进制数,可以看成是一个模 2n 的系统
如何定义补码?
在前文中,我们了解到并可以知道非负数的算术运算都是没有问题的
接下来设想一个正数加上一个负数,由前文可知,在模 2n 的系统中,加上一个负数 −a,就相当于加上该负数的补,即 2n−a,我们将这个数定义为负数的补码
如何计算负数的补码?
定义一个十进制整数 x 的原码(以八位为例)为 a7a6a5a4a3a2a1a0(ai=0,1,i=0,1,2⋯,7)
依据前文定义,正数的补码就是它本身,也即当 x⩾0 时,[x]补=a7a6a5a4a3a2a1a0
接下来我们考虑负整数,即当 x<0 时,[x]补=2n−x=[(28−1)−x]+1,我们将 2n−1 和 x 都写成二进制的形式,并做差可得
−1a7aˉ71a6aˉ61a5aˉ51a4aˉ41a3aˉ31a2aˉ21a1aˉ11a0aˉ0
注意这里 1−ai=aˉi 无非就两种情况,列举即可得到这个等式成立
由此,我们便得到了负数的补码在二进制数下的计算公式
[x]补=[x]反+1
数据溢出
为了方便,接下来我们以四位二进制整数为例
根据前面的知识,四位二进制表示的整数的范围为 [−8,7],将这些整数和对应的补码表示在数轴上如下
我们发现 7+1 本应对应 8,然而由于 0111+0001=1000,而使得结果为 −8,虽然不符合常理,但是这样就限制了所有的数的范围都在 [−8,7] 这个区间内,同时,我们把 7+1=−8 这种情况称为溢出,特殊地,这是一个上溢出(8 比最大值 7 大)
同样地,7+2 也不会等于 9,而是等于 −7,换成数数的算法,我们可以知道 7 的后一位是 −8,7 的后两位是 −7,以此类推得到 7 的后 9 位回到了 0
那么,我们不妨将这些数放在一个圆上(将数轴绕成一个圆),就可以得到下面的这种表示
按照上面数数的算法,我们知道 7 的后 9 位是 0,也就是 7+9=0 或者 −8+1=0,我们现在来分析这两个式子
先看后者,−8 在计算机中的表示为 1111,1 的表示为 0001,按理说 1111+0001=10000,然而,现在我们只有四位二进制数,那么多出一位的 1 其实就是没有意义的,因此我们直接取后四位 0000,也就是 0
再看前者,通过转化为二进制计算很容易得到这个结果(如下,取结果的后四位),但是这显然有点慢了
+1010100100110
换个想法,这个等式告诉我们在四位的二进制中 16 会溢出为 0,现在想说明这件事情也不难,无论是从上文我们推导得出的,还是本节我们把数轴绕成圆(看成一个有 16 格的时钟),都能知道这是一个模 24=16 的系统,因此只需要用 16 除以 16 取余数,就能得到 16 在 0~7~−8~0 对应的次序,余 0 表示对应第一个数,即 0
下面来看一个计算溢出的示例
一个八位的的二进制数定义为 −257,那么其溢出后的值为多少?
- 我们考虑用 −257 除以 28=256,即商 −2,余 255,对应在圆上从 0 开始计数 255 次,最后得到的是 −1
- 其实,如果把这个计算类比成上面钟表(此处为 256 格)的计算,指针从 0 开始,往回(逆时针)走 257 格,也就是先走 256 格后回到 0,再往回走一格到 −1