补数到底是个什么玩意儿?从根儿上理解一下

小贴士:

1. 此文节选自小孩子写的《计算机是怎样运行的》,想从根儿上理解计算机,先从根儿上理解一下补数吧~

2. 此文较长,建议收藏 + 在电脑上观看效果最佳。

十二进制中的补数

相信各位同学都知道怎么通过钟表看时间,比方说下边的钟表显示的时间就是 6点整

在时钟的表盘上一共有12个时针刻度,我们可以认为时钟是以十二进制来进行计数的。钟表设计者考虑到大部分用户的感受,表盘时钟刻度旁的数字是十进制形式的,我们决定把它们替换成十二进制形式的数字。假设十二进制中采用 0~9 a b 这12个符号来表示数值,其中 0-9 的字面量和十进制中的相同,使用 a 代表十进制中的 10 b 代表十进制中的 11 。另外,我们决定从 0 开始计数,所以把普通钟表中的 12 替换为 0 ,然后钟表就变成了这个样子:

忽然发现现在的北京时间实际上是 3点整 ,那该怎么调整一下钟表呢?我们可以采用下边两种方法之一来达到目的:

  • 方法一:将时针逆时针拨3个小时。

    从数学的角度看这个事情,其实是一个求6个小时减去3个小时等于几的问题,可以使用下边的式子表达这个过程:

    6 - 3 = 3
    
  • 方法二:将时针顺时针拨9个小时。

    从数学的角度看这个事情,其实是一个求6个小时加上9个小时等于几的问题,可以使用下边的式子表达这个过程:

    6 + 9 = 0 + 6 + 9
          = 0 + (6 + 6) + 3
          = 0 + 10 + 3  //十二进制表示,十二进制的10代表十进制的12
          = 13  //十二进制表示,十二进制的13代表十进制的15
    

    也就是说6个小时加上9个小时相当于将时针从0刻度先顺时针转6个刻度,然后再转9个刻度,还相当于从0刻度先顺时针转12(十二进制中的 10 )个刻度(也就是表盘的一圈),然后再转3个刻度。这里我们只在乎时针处在哪个刻度上,而不在乎它到底是否多转了一圈,所以可以直接把进位给删掉,单纯地从钟表时间的效果上看起来下边的式子就是成立的:

    6 + 9 = 3
    

    惊不惊喜,意不意外, 6 + 9 的结果竟然变成了 3 ,效果和 6 - 3 是一样一样的。其实回头想想我们只是玩了个小把戏,我们只是忽略了那个十二进制加法结果中的进位罢了~

钟表一共就12个刻度,使用单个十二进制位就可以表示这些刻度。3和9的和正好是十二进制中的 10 (也就是十进制中的 12 ),我们可以说在使用单个十二进制位表示数字时,3和9互为补数。在这个例子中我们看到,减去一个数等于加上这个数的补数然后再减去十二进制中的 10 (也就是十进制中的 12 ),减去十二进制中的 10 的操作其实就相当于直接把进位忽略掉,得到的就是原减法的结果。这个过程给我们提供了一个思路: 减法可以通过适当的方式转换为加法进行运算

十进制中的补数

我们上边只是以钟表为例提出了一个在使用单个十二进制位表示数字时 补数 的概念,这远远不够,下边我们正式以十进制为例全面的介绍一下到底啥是个 补数

在我们采用十进制进行计数的情况下,如果两个使用n个十进制位表示的数字之和为10 n ,那我们称这两个数互为补数,比方说:

  • 1 9 都是使用 1 个十进制位表示的,它们的和是10(也就是10 1 ),所以它们互为补数。

  • 11 89 都是使用 2 个十进制位表示的,它们的和是100(也就是10 2 ),所以它们互为补数。

  • 111 889 都是使用 3 个十进制位表示的,它们的和是1000(也就是10 3 ),所以它们互为补数。

那这里就有个问题了,3位十进制数 999 的补数是啥?按理说 1000 - 999 = 1 ,所以 999 的补数就是 1 吗?那 1 9 不是互为补数么,咋又跑出个 999 ?哈哈,切记我们在讨论一个数的补数时 一定要限定一下我们当前使用几个十进制位来表示数字 ,如果我们限定使用3个十进制位来表示数字的话,那么 999 的补数也应该是由 3 个十进制位表示的,其实只需要给十进制数字 1 前边补两个 0 ,变成 001 就好了,我们就可以说在使用3个十进制位表示数字时, 999 的补数就是 001 (虽然 001 1 在数量大小上没有区别)。

如何求一个数的补数

在限定了使用n个十进制位来表示数字之后,怎么求一个数的补数呢?最容易想到的方法就是使用10 n 减去这个数。比方说在我们限定了使用3个十进制位来表示数字之后,如果我们想求145的补数,那就使用1000(也就是10 3 )来减去这个145。

  1000
-  145
--------
   855

不过这个过程需要使用到借位,如果我们想用更简单的方式计算出结果的话,可以使用下边两个步骤来计算:

  • 步骤一:使用 999 减去 145

       999
    -  145
    --------
       854
    

    这个过程不需要使用到借位,可以很轻松的算出 999 - 145 的结果是 854

  • 步骤二:然后再将步骤一中的结果加 1

       854
    +    1
    --------
       855
    

    这个 855 就是 1000 - 145 的差。

通过使用这两个步骤,我们成功的避免了减法中的借位,很轻松的算出了145的补数855。其实这个过程本质上就是这样的:

1000 - 145 = 999 + 1 - 145
           = (999 - 145) + 1
           = 854 + 1
           = 855

一般地,在我们限定了使用n个十进制位来表示数字之后,可以通过下边两个步骤来求一个数的补数:

  • 步骤一:先计算由n个 9 组成的十进制数字(其实也就是 10n - 1 )减去该数的值(此过程不涉及借位)。

  • 步骤二:然后将上一步骤的结果加1。

利用补数将减法转换为加法

我们现在也知道了一个数的补数该如何更快的计算出来,不过引入这个 补数 的概念有啥用呢?哈哈,它就是解决减法问题的关键。比如我们在使用1个十进制位表示数字的时候,3和7互为补数。假设现在我们想计算 6 - 3 的值,那我们可以这样转换这个表达式:

6 - 3 = 6 - (10 - 7)
      = 6 + 7 - 10

也就是说: 在做只使用1个十进制位表示的数字之间的减法操作时,减去一个数等于加上这个数的补数然后再减去10,减去10的操作其实就相当于直接把和的进位忽略掉,得到的就是原减法的结果 6 + 7 的和是13,如果我们直接忽略掉进位,那么结果就成了 3 ,它其实就是 6 - 3 的结果。如图所示:

更一般地,假设 是两个使用n个十进制位表示的数字,它们互为补数,那么我们有:

也就可以推出:

那么假设 也是使用n个十进制位表示的数字,那么我们有:

也就是说: 在做使用n个十进制位表示的数字之间的减法操作时,减去一个数等于加上这个数的补数然后再减去10 n ,减去10 n 的操作其实就相当于直接把和的进位忽略掉,得到的就是原减法的结果 。我们再举几个例子:

  • 假设x的值为 215 ,y的值是 145 ,我们想求 x - y 的差是多少,就像这样:

      215   //x的值是215
    - 145   //y的值是145
    --------
        ?
    

    首先计算 145 的补数是 855 ,然后将被减数 215 855 相加:

      215
    + 855
    --------
     1070
    

    然后减去1000(也就是10 3 ),减去1000的操作也可以相当于直接把和的进位忽略,得到如下结果:

    去掉前缀的 0 ,相当于说 x - y 的差就是 70

  • 假设x的值为 215 ,y的值是 45 ,我们想求 x - y 的差是多少,就像这样:

      215   //x的值是215
    -  45   //y的值是45
    --------
        ?
    

    因为 45 是使用2个十进制位表示的,所以我们先把它扩展为使用3个十进制位表示的形式(其实就是在数字前加前缀 0 ),所以减法看起来就是这样:

      215   //x的值是215
    - 045   //y的值是045
    --------
        ?
    

    首先计算 045 的补数是 955 ,然后将被减数 215 955 相加:

      215
    + 955
    --------
     1170
    

    然后减去1000(也就是10 3 ),减去1000的操作也可以相当于直接把和的进位忽略,得到如下结果:

    相当于说 x - y 的差就是 170

哇唔,是不是很简单,减法操作顺利的被转换成了加法操作。

负数的表示

如果大家仔细看的话,我们上边举的例子中的被减数都比减数大,如果被减数比减数小的时候会发生什么事情呢?依据初高中的数学知识,我们意识到很显然运算结果将得到负数,不过我们还是好奇如果还沿用上述将减法转化成加法的方式去计算的话会发生什么?比方说我们假设x的值为 145 ,y的值是 215 ,我们想求 x - y 的差是多少,就像这样:

  145  //x的值是145
- 215  //y的值是215
--------
    ?

首先计算 215 的补数是 785 ,然后将被减数 145 785 相加:

  145
+ 785
--------
  930

然后再减去1000(也就是10 3 )么?930比1000小呀,我们不能采用上述舍去进位的方式来求得最后结果,该咋办呢?我们可以想到下边这些方式来解决这种被减数比减数小的情况:

  • 方案一:人为地规定不允许出现减数比被减数大的情况。

    这种解决方案有点那种:“不解决问题,解决提出问题得人”的意思,过于硬核~

  • 方案二:假设在做使用n个十进制位表示的数字之间的减法操作时,对于x为被减数,y为减数,并且x<y的情况,我们可以变换一下上边的公式:

    对于 这个式子来说,本例中 的值我们已经算出来了,就是930。用 减去 就相当求 的补数,也就是我们现在只要求出930的补数,然后再在结果前边加个符号 - 就好了。 930 的补数就是 070 ,代表的数值就是 70 ,最后在补数前边加个负号就是: -70 ,也就是说 -70 就是最终的结果。

    上边的过程像是一个脱了裤子放屁的过程,既然使用补数去计算 145 - 215 的差不好算,直接算 -(215 - 145) 不就好了~ 是的,没啥问题。其实不论是取最终结果的补数还是在运算前就调换被减数和减数的位置,我们都在十进制中引入了一个新的符号:负号 - ,这个符号是独立于 1、2、3、4、5、6、7、8、9 这十个符号的新符号。对于人来说引入一个负号没什么问题,不过我们想让机器代替我们思考,最好不要让有负号这个东西,于是有了方案三。

  • 方案三:在使用n个符号表示数字时,对于一个正数 ,直接用它的补数 来表示与其绝对值相同的负数。

    本例中我们可以把 070 当作一个正数,而它的补数 930 当作一个负数(其实就相当于我们眼中的 -70 ),此话怎讲呢?且听稍后细细分解~

在一些同学眼里, 930 明明就是 930 ,你为啥要说它是 -70 呢?这其实是我们固有的观念在作怪,我们从小接受的教育就是只有数字前边带负号的才算是负数。请先抛弃一下这种固有观念,我们这里不想引入负号 - 这个符号,所以只能想其他方式来表示负数。我们先看一下使用3个十进制位表示的数字在数轴上的分布:

我们可以以 500 为中心把这些数字分成两组(左边的那组 000 除外),也就是 001~499 为一组, 501~999 为一组。散布在两个组里的数字以 500 为中心依次互为补数,如图所示:

这时候我们可以人为地规定、人为地规定、人为地规定(重要的事情说三遍,是人为的规定):

  • 001~499 这一组的数字为正数。

  • 501~999 这一组的数字为负数,并且对于 001~499 这一组中任意一个正数 ,直接用它的补数 来表示与其绝对值相同的负数。

  • 000 代表的值就是 0

501~999 这一组与我们固有观念中的负数的映射情况就如下图所示:

不过这上边还有一个事情没有处理,就是 500 在数轴上的位置比较尴尬,它左边的数代表非负数,它右边的数代表负数。而且 500 的补数还是 500 自己( 1000 - 500 = 500 ),那我们应该把 500 当作一个正数,还是应该把它当作一个负数( -500 )呢? 其实都可以 ,我们想让它是正数就是正数,想让它是负数就是负数(也就是取决于人为的规定)。不过常规上我们还是把它看作一个正数的补数,也就是把它当作 -500 看待。之所以采用这个规定。是因为我们想做一个方便的处理: 我们可以依据某个数字的首个十进制位来判断它是正数还是负数 ,规则如下:

  • 如果首个十进制位的字面量小于 5 ,那么我们就把它当作是一个非负数。

  • 如果首个十进制位的字面量大于或等于 5 ,那么我们就把它当作是一个负数。

为了方便起见,我们可以把首个十进制位称之为 符号位

引入了 符号位 的概念之后,我们需要修改一下上边的映射图:

更一般地,当我们之后再看到使用n个十进制位表示的数字的时候,它代表的数值大小需要按照下边的规则去判断:

  • 如果符号位的字面量小于 5 ,那么我们就把它当作是一个非负数。

  • 如果符号位的字面量大于或等于 5 ,那么我们就把它当作是一个负数,它的绝对值和它对应的补数相同。

好了,我们重新梳理一下我们说了半天到底说了个啥。我们其实可以把使用n个十进制位表示的数字看作是一堆十进制符号的组合,我们现在一共有10个十进制符号: 0、1、2、3、4、5、6、7、8、9 ,那么三个十进制符号的组合就一共有 10×10×10=1000 种可能,,其实也就是: 000~999 。每一种组合都可以映射到一个数值,不过我们可以有不同的映射方式:

  • 我们最初介绍进制的时候认为对由三个符号组成的组合来说,每个位置的符号表示的数值为该符号的字面量乘以一个10的整数次幂,整个组合表示的数值为所有位置的符号代表的值加起来的总和。这个过程中没有引入 负数 的概念,我们可以把通过这样的方式来解释数值的符号组合称为 无符号数 。对于由三个符号组合而成的 无符号数 来说,它真正代表的数值范围也就是 0~999 这1000个数。

  • 当我们引入 负数 的时候,我们把首个符号的字面量小于 5 的符号组合代表的数值和 无符号数 中的含义一样,但是首个符号的字面量大于或等于 5 的符号组合当作是一个负数,它的绝对值和它对应的补数相同。我们把通过这样的方式来解释数值的符号组合称为 有符号数 。对于由三个符号组合而成的 有符号数 来说,它真正代表的数值范围也就是 -500~499 这1000个数。

好了,我们稍微总结一下我们说了半天到底说了个啥。我们现在一共有10个十进制符号: 0、1、2、3、4、5、6、7、8、9 ,那么三个十进制符号的组合就一共就有 10×10×10=1000 种可能,其实也就是: 000~999 。每一种组合都可以映射到一个数值,不过我们可以不同的映射方式:

也就是说,对于同一组符号组合,把它解释成有符号数和无符号数的时候它所代表的数值可能相同也可能不同。比方说对于三个符号的组合 215 ,它的首个符号的字面量是 2 ,小于 5 ,把它解释成无符号数和有符号数的数值大小都一样。不过对于三个符号的组合 930 ,它的首个符号的字面量是 9 ,大于 5 ,把它解释成 无符号数 的数值就是正数 930 ,而把它解释成 有符号数 的话就代表数值 -70 。至于什么时候把一组符号组合解释成 无符号数 ,什么时候解释成 有符号数 ,取决于我们自己~

扩展表示数字的十进制位个数

在我们采用十进制进行计数的情况下,当表示数字时使用的十进制位个数不同时,能表示的数值范围也就不同,比方说:

  • 如果我们使用1个十进制位表示数字(也就是 0~9 ):

    • 能表示的 无符号数 的数值范围就是 0~9

    • 能表示的 有符号数 的数值范围就是 -5~4

  • 如果我们使用2个十进制位表示数字(也就是 09~99 ):

    • 能表示的 无符号数 的数值范围就是 0~99

    • 能表示的 有符号数 的数值范围就是 -50~49

  • 如果我们使用3个十进制位表示数字(也就是 000~999 ):

    • 能表示的 无符号数 的数值范围就是 000~999

    • 能表示的 有符号数 的数值范围就是 -500~499

  • 如果我们使用4个十进制位表示数字(也就是 0000~9999 ):

    • 能表示的 无符号数 的数值范围就是 0000~9999

    • 能表示的 有符号数 的数值范围就是 -5000~4999

  • 等等等等~

很显然,使用越少的十进制位来表示数字时,能表示的数值范围就越小;使用越多的十进制位来表示数字时,能表示的数值范围就越大。在我们使用较多的十进制位来表示某个数值时,可能无法使用更少的十进制位来表示这个数值;相反的话,在我们使用较少的十进制位来表示某个数值时,那么一定能使用更多的十进制位来表示这个数值。

如果我们想将使用较少的十进制位表示的无符号数扩展成使用更多十进制位表示的无符号数,我们只需要在开头补更多的0,比方说使用三个十进制位表示的无符号数 501 ,如果我们想:

  • 把它扩展成使用4个十进制位表示的无符号数,简单地在开头补1个0就好,变成这样: 0501

  • 把它扩展成使用5个十进制位表示的无符号数,简单地在开头补2个0就好,变成这样: 00501

  • 把它扩展成使用6个十进制位表示的无符号数,简单地在开头补3个0就好,变成这样: 000501

  • 把它扩展成使用n(n > 3)个十进制位表示的无符号数,简单地在开头补 n-3 个0就好。

我们可以把这种扩展方式称之为 零扩展 ,在原无符号数开头简单地补0,并不会改变它代表数值的大小。

不过在扩展表示 有符号数 的十进制位个数的时候就不能简单的使用 零扩展 了,比方说对于使用3个十进制位表示的有符号数 501 ,它的符号位为 5 ,说明它代表的是一个负数,代表的数值其实是 -499 。如果我们想使用4个十进制位表示这个有符号数,要是直接在开头补0的话就变成了: 0501 ,扩展后的有符号数的数值变为了正数 501 ,这可不是我们期望的。其实求使用3个十进制位表示的有符号数 501 扩展到使用4个十进制位表示的有符号数是什么的本质就是求在使用4个十进制位表示数字时, 0499 的补数是什么。求一个十进制数的补数还不简单么?求解过程分两步:

  • 步骤一:使用 9999 减去 0499 ,得到 9500

  • 步骤二:将步骤一中的结果 9500 1 ,得到 0499 的补数其实就是 9501

也就是说使用4个十进制位表示数值为 -499 的有符号数就是 9501 ,而使用3个十进制位表示数值为 -499 的有符号数是 501 9501 501 的区别是个啥?就是在 501 前边补了个 9 就好了嘛~

我们可以换一种角度来看这个问题, 499 501 的和本来已经是 1000 了,现在要求 0499 在使用4个十进制位表示的补数是多少,我们只要在1000的基础上再加9000就好了嘛,所以我们直接在 501 前边补一个 9 ,变成 9501 就是扩展后的有符号数。那如果我们现在想把 501 扩展为使用5个十进制位表示的有符号数咋办呢?当然是在前边补2个 9 了。依次类推,如果我们现在想把 501 扩展为使用n(n>3)个十进制位表示的有符号数咋办呢?当然是在前边补 n-3 个9了。

如果某个有符号数表示的数值是一个非负数,比方说使用3个十进制位表示的有符号数 301 ,它的符号位的字面量小于 5 ,肯定表示一个正数,那么在我们扩展使用的十进制位个数时,只需要简单的在该数前边补0就好。比方说现在将其扩展为使用4个十进制位表示的有符号数,直接在原数前边补1个 0 变为 0301 就好~

总结一下:扩展表示有符号数的十进制位个数的时候,在该有符号数的符号位的字面量小于5的时候,可以采用直接在原数前边补0的方式扩展;在该有符号数的符号位的字面量大于或等于5的时候,可以采用直接在原数前边补9的方式扩展。我们把这种扩展时依赖符号位的值的扩展方式称之为 符号扩展

小结

好的,到此为止,关于十进制中的补数就说的差不多了。说出来大家可能想打我,毕竟小孩子是写计算机相关技术的,上边所唠叨的十进制中的补数全部都是铺垫,目的只是为我们在学习二进制中的补数中可以依葫芦画瓢,让学习曲线更平滑一点(毕竟我们平时接触十进制更多一点)。关于二进制的中的补数和相关问题,请持续关注《计算机是怎样运行的》这本书哈哈哈哈哈。

小孩子曰: 小孩子为了保证让同学们的学习曲线尽量平缓,写书慢的一匹,再过半年可能都成不了书,先去看MySQL吧(点击原文观看)~ 。

小青蛙历史文章(历史文章,不容错过):

量子波动速读?真是笑死我了

代表程序员气质的,是秃头还是大胡子?

收藏版MySQL语句加锁分析

虚拟内存是个啥 | 一分钟系列

MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!

长按关注小青蛙,都是干货喔