为何整型范围中负数比正数多一个?

发布时间 2023-10-18 21:44:27作者: DawnTraveler

1.问题

如图所示,整型范围中,负数均比正数多一个?

2.解决方案

引用博客链接:https://juejin.cn/post/7128196204655018014

2.1引子

所有的负数范围都比整数多 1 个数字,其实这是计算机的存储和加减运算机制决定的。

  1. 首先,计算的存储只有 0 和 1,每个位置要么存 0,要么存 1,这些位置又叫做位(即 bit)。
  2. 其次,拿 byte 举例,它在目前计算的标准中是 8 位的,也就是说:1 byte = 8 bit;所以一个 byte 在计算机中只有 8 个可以存放 0 和 1 的位置,8 个位置放上 0 或 1,穷举的全部可能性为 2 的 8 次方,即 2^8=256。
    所以,在一个 byte 中最多只能表示 256 个不同意义的事物(可以是任何可能的事物),在这里如果是数字的话,就只能有 256 个数字了。如果我们不需要用它表示负数,那么它可以表示 0至255 这 256 个数字;如果它需要标识正负,计算机中会用高位表示符号,其他位表示数字,而 0 表示正号,1 表示负号。这时候 0 开头的二进制数字表示的范围是 0至127,而 1 开头的二进制数字表示的范围是 -0至-127,所以被正负号占用一位后,实质只能表示 -127至127 这 255 个数字而不是 256 个数字。
  3. 但是,0 和 -0 本质上是没什么区别的,如果能够将 -0 改为代表其他数字,那么表示的范围就能增加,所以就有了:-0 表示 -1、-1 表示 -2、以此类推到 -127 表示 -128 这样的情况。

虽然上述可以增加范围,但实际中不会有人为了增加一个数字范围而改变这种人类不容易理解的表达方式的,之所以实际情况确实如此表示,是有深层原因的:计算机人员为了简化计算机的计算过程和提高效率,不想让计算机先判断数字的正负,再使用加法或减法来计算,而是简化成:计算机只需要进行加法操作,并且忽略正负号的识别。

2.2 引入反码(将减法变为加法)

在反码计算时,我们希望将符号位也带入计算,这样原来的负数便会
要达到这个目的的理论很简单,就是 1-2 可以表示为 1+(-2) 的,还是用 byte 举例,

第一步:1-2=-1  这种适合人类思维的计算过程是:00,000,001 - 00,000,010 = 10,000,001(十进制为 - 1)

这里是识别正负号再人工计算的,这对于计算机来说不够简单,效率也不高;

第二步:1-2 改为:1+(-2)=-1 后,其计算过程为:00,000,001 + 10,000,010 = 10,000,011(十进制为 - 3)

显然这个计算过程是不正确的,为了能够直接做加法,还需要对负数取反后计算,即将负数除了符号位,其他位全部取反,10,000,010 取反后就是 11,111,101—— 这种取反后的二进制码也叫反码,取反前的二进制码则叫做原码。(顺便说明下反码的转换过程:正数的反码是其本身,负数的反码是除了符号位外其他位全部取反;反码转正码只需要逆转此过程)

第三步:将原码的计算过程取反后的计算过程为:

(原)00,000,001 + (原)10,000,010 = (原)10,000,011(十进制为 - 3)

(反)00,000,001 + (反)11,111,101 = (反)11,111,110(转为原码为 10,000,001,十进制为 - 1),计算过程正确。

可见反码的出现完全是为了简化计算机的底层计算而存在的,这种计算方式忽略了符号仅使用加法就解决了正负数的问题。

反码原理

https://blog.csdn.net/quyanyanchenyi/article/details/108961126

1.十进制减法

通常我们用十进制进行减法的时候通常会出现以下两种情况:
不需要借位:如“456-123”
需要借位: 如“253-176”
对于不需要借位:我们可以直接相减即可
对于需要借位的:我们可以通过一系列操作来免去借位
如:
253 - 176 = 253 + (999 - 176) + 1 - 1000
=1077 - 1000
=77
// 此处我们 通过引入 9 的补数 来避免借位

上面我们引出了补数的概念,一个数的补数即为:在这个数所处的进制系统里,这个数的每位最高位 - 这个数 + 1

在这里我们运用了:“被减数 - 减数 = 被减数 + 减数的补数 - 减数每个位最高位
————————————————————————————————————
那么对于结果是负数的该怎么办呢,比如:“176-253”
我们仍用上述的方法来试一下::
176-253 = 176 + (999 - 253) + 1 - 1000
======= = 922 - 999
// 我们可以看到这里还是得借位,不过我们只要把减数和被减数调换位置,然后在前面加上负号就可以了:-(999 - 922)

在被减数小于减数的时候,我们仍然运用了:“被减数 - 减数 = 被减数 + 减数的补数 - 减数每个位最高位,只不过最后将减数和被减数调换了一下位置,然后再加个符号

2.二进制减法

上述例子:”253 - 176“ 的二进制减法:

    11111101
   -10110000
  -------------------
  =11111101 + (11111111 - 10110000) + 1 - (100000000)
  =101001101 - 100000000
  =1001101
  =77(十进制)

不知道聪明的小伙伴发现了没有,在二进制系统中求一个数的补数,其实没有必要使用减法,由于二进制系统只有”0“,”1“构成,求一个数的补数只需要把那个数的0变成1,1变成0即可,所以上述过程可以简化为:

    11111101
   -10110000
  -------------------
  =11111101 + (01001111)+ 1 - (100000000)
  =101001101 - 100000000
  =1001101
  =77(十进制)

对于 ”176 - 253“:

    10110000
   -11111101
  -------------------
  =10110000+ (00000010)+ 1 - (100000000)
  =10110010 - 11111111
  = -(11111111 - 10110010)
  =- 77(十进制)

3. 减法该如何实现呢?

前面我们一直在讨论怎么把减法变成加法,但那只是我们在脑海中的运算,至于在计算机中,我们还没有说明,在说明之前,我们先来讨论一下”负数该如何表示“?

负数的表示方法

像十进制中用”+“,”-“表示?,显然是不太可行的,因为二进制中只有0和1
用0表示正,1表示负?好像可行,但是还远远不够
通常用来表示负数和正数的方法,他的好处在于能给表示除所有的正数和负数。我们将0想象为这个无限妍伸序列的中点:
…-99999999, -99999998… -3, -2, -1, 0, 1, 2, 3…9999999,10000000,…999999999…

以支票账户为例,这里人们通常会遇到负数。假设我的账户有499元,可透支500元,那么我们能处理的额度为:-500 - 499,这个约束说明只用三位十进制,而不用负号就能表示我们所有的数字,但我们并不需要500——999之间的整数,因为我们所需要的最大值为499,那么我们可以用500-999之间的数表示负数:

-500,-499,-498.。。。-4,-3,-2,-1 0 1,2,3,4…498,499 可以表示为
500 , 502 , 503 996 997 998 999 0 , 1 ,2 ,3 ,4 …499

注意:这就形成了一个循环排序。最小的负数(500),看起来像是最大整数(499)的延续。而数字999(实际是-1)是比0小1的第一个负数,如果我们在999(-1)上加上1,则会得到1000,由于我们处理的是三位数,这里则是0.

我们可以把这些数字想象成钟表上的刻度,时钟的十二点初代表0,顺时针有 1000个点,最后一个点是999:

而原先则是12点处为0,顺时针为正,逆时针为负:

这种标记法称为10的补数,为了将三位负数转换成10的补数,我们用999减去它再加一:
如:-255 = (999-255) + 1 = 745;

假设你游一个余额为148元的账户,开了一张78元的支票,那就意味着你的可操纵钱变为了:148 - 78,也就是(-78) + 148;-78对于10的补数为(999-78+1)= 922,那么新余额为:(148+922 )-1000= 65(忽略溢出)。如果我们又开了一张150美元的支票,需要在余额上减去150,(-150)等于(999-150+1) = 850,新余额为(65+850)-1000=-85

上面的减去1000是因为这个三位十进制系统最大容量为1000,超过1000就要溢出,这也解释了上面二进制和十进制减法为什么最后要减去一个数,那个数就是系统的最大容量(或许当时你看的时候就发现疑问了,因为当时我们只是用一种特殊的方法来避免做减法,而没有把他放进一个系统中看待,现在我们有了模型,就会更容易的理解为什么减法可以换成加法了)。

这种机制在二进制系统中称为二的补数,以八位二进制系统为例:00000000——11111111代表0 —— 255,此处无符号位:

但是,如果我们还想表示负数的话,我们可以把左边第一位做为符号位,其他七位用来计数,则就可以表示:”0——127“ 和 ”-1—— -128“。为了计算2的补数,我们只需要计算出1的补数再加1即可。

比如我们要计算-127 和 124 相加,即”1111 1111“ + ”0111 1100“ ,对”1 1111111“求补:”1 0000001“
变为”10000001“ + ”0111 1100“ = (1 1111101) = - 3

要注意的是,这里涉及到了上溢和下溢的情况(结果>127或<-128),比如125 + 125 = -6,这是由于我们规定所处理的数值为8位,因此最左位常常被忽略。右边八位相当于十进制的6.

一般来说,如果两个操作数符号相同,结果的符号数与操作数符号不同,则这样的加法是无效的

因此我们在做加减法时,必须要提前算一下数字所使用的范围,这也是可溢出计数系统的缺点。

2.3 引入补码(解决0和-0的问题)

反码相较于原码进行了优化,但还存在某些问题,如果上面的 1-2 是 2-2 的话,那么结果就是下面的:

(原)00,000,010 + (原)10,000,010 = (原)10,000,100(十进制为 - 4)
(反)00,000,010 + (反)11,111,101 = (反)11,111,111(转为正码为 1,000,000,十进制为 - 0),
    终于说到这个 -0 了,这个从人的角度来看当然就是 0 的意思,但是对于计算机来说,还需要编指令告诉它:-0 就是 0,不然在计算机眼里它是两个不同的数字或信息。
这样一来,就又是增加了计算机的复杂度,所以为了方便和效率,人们想出让 -0 表示-1,-1 表示 -2…… 这样的方式,结果就使得负数会比正数多一个数字的情况了 
——所以使用这种表达方式的原因并不是为了多增加一个数字范围,而是降低计算机的计算复杂度,毕竟前者相比后者,根本不值一提。

但是使用了这种表示方式后,计算机要怎么计算呢?
这时候就需要用到补码了。(知识点来了,补码的转换过程:正数的补码是其本身,负数的补码是除了符号位外其他位全部取反后加 1,也就是反码再加 1;补码转正码或反码只需要逆转此过程)

第四步:
        这时候我们要讲清楚的是 -0 的问题,所以我们拿 2-2 的例子来说明,将原码转为补码后的计算过程为:
(原)00,000,010 + (原)10,000,010 = (原)10,000,100(十进制为 - 4)
(反)00,000,010 + (反)11,111,101 = (反)11,111,111(转为正码为 1,000,000,十进制为 - 0)
(补)00,000,010 + (补)11,111,110 = (补)100,000,000(转为正码前需要舍去高位 1,因为我们本来就是只有 8 位的,现在变成 9 位了,是存不下的,结果为 00,000,000,十进制为 0)

     这时候就再也没有 -0 出现了,而舍位操作并没有对计算过程造成任何影响,并且还将 -0 转成了 0(100,000,000 是 - 0,00,000,000 则是 0)

补码原理:

3. 总结

因为计算机最终是保存补码进行计算的,这样表示的负数就总会比正数多一个数字,而 Java 也是直接使用计算机的这套规则的, 自然结果都是一样的。

而要是简单回答这个问题,那么可以简单归纳为:因为原码、反码、补码的规则导致了 Java 整型的负数比正数多一个数字范围。