面试题 - (0.1+0.2=0.3)?

面试题 - (0.1+0.2=0.3)?

  • 题目
  • 解答
    • 0.1 转二进制
    • 0.2 转二进制
    • 浮点数
    • 浮点数运算
  • 总结

题目

0.1 + 0.2 是否等于 0.3 ?

解答

首先直接通过浏览器开发者模式打开控制台看一下结果。
在这里插入图片描述
其实看到这个题目应该就可以猜到肯定不可能是 0.3,否则就不会出这个题目了。这个题目涉及到数学运算中的浮点运算。

0.1 转二进制

0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + …

左右两边一直乘以2,将小数与正数分开,得到以下结果。

0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + … (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + … (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + … (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + … (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + … (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + … (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + … (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + … (h = 1)

这个计算在不停的循环,所以 0.1 用二进制表示就是 0.00011001100110011……

0.2 转二进制

0.2 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + …

左右两边一直乘以2,将小数与正数分开,得到以下结果。

0 + 0.4 = a * 2^0 + b * 2^-1 + c * 2^-2 + … (a = 0)
0 + 0.8 = b * 2^0 + c * 2^-1 + d * 2^-2 + … (b = 0)
1 + 0.6 = c * 2^0 + d * 2^-1 + e * 2^-2 + … (c = 1)
1 + 0.2 = d * 2^0 + e * 2^-1 + f * 2^-2 + … (d = 1)
0 + 0.4 = e * 2^0 + f * 2^-1 + g * 2^-2 + … (e = 0)
0 + 0.8 = f * 2^0 + g * 2^-1 + h * 2^-2 + … (f = 0)
1 + 0.6 = g * 2^0 + h * 2^-1 + i * 2^-2 + … (g = 1)
1 + 0.2 = h * 2^0 + i * 2^-1 + j * 2^-2 + … (h = 1)

0.2 用二进制表示就是 0.00110011001100110……

浮点数

  1. 整数型存储整数,浮点型存储小数。显示浮点数的方法有两种,单精度和双精度。单精度用 32 位表示,双精度用 64 位表示。JavaScript 是遵循国际 IEEE 754 标准,将数字存储为双精度浮点数,也就是用 64 位表示。

  2. 最大数和最小数一般用科学计数法来表示。比如 0.000045 可以表示为 0.45 * 10^4。某市有 10000000 人口可以表示为 1 * 10 ^7。科学记数法主要是为了书写方便。

  3. 对于二进制也是一样,以 0.1 的二进制 0.00011001100110011…… (后面有0.1转二进制的过程)这个数来说:
    可以表示为:1 * 2^-4 * 1.1001100110011……
    二进制科学计数法公式为:V = (-1)^S (1 + Fraction) 2^E

  4. 所有浮点数都可以用以上公式来表示,所以我们只需要把变化的 S,Fraction 以及 E 存储即可。以 64 位存储为例,其中用 1 位存储 S(sign,存在位 63 上,表示符号位,取 0 表示正数,1表示负数)。用 11 位存储 E+bias,存在位 52-62 上。用 52 位存储 Fraction,存在位 0-51 上。

综上所述,上文 1 * 2^-4 * 1.1001100110011…… 例子中 ,
Sign(该数为正数)为 0。
Fraction(小数部分)为 1001100110011……。
Exponent(指数)为 -4。
bias (这里是为了辅助存储 E,因为要存11位,如果只是正数的话是 2^11 -1 = 2047,范围是 0 - 2046。但是可能存在负数,所以取值范围为 -1023-1023,为了不存负数,存储的时候需要加 1023( 2^(11-1)-1=1023),取值的时候再减去1023)。所以 E+bias = -4+1023 = 1019。而 1019 的二进制为 1111111011。
所以 0.1 用 64 位二进制表示如下:
0 01111111011 1001100110011001100110011001100110011001100110011010
同理,0.2 用 64 位二进制表示如下:
0 01111111100 1001100110011001100110011001100110011001100110011010

浮点数运算

浮点数运算有五个步骤:对阶、尾数运算、规格化、舍入处理、溢出判断。

  1. 对阶就是把阶码对齐,其目的是为了使两个浮点数的尾数进行加减运算。比如 0.1 的二进制科学记数法是 1.1001100110011…… * 2^-4,阶码就是 -4。而 0.2 的二进制科学记数法是 1.10011001100110...* 2^-3,阶码就是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。也就是 0.1 的 -4 调整为 -3,对应变成 0.11001100110011…… * 2^-3

  2. 尾数运算
    0.1100110011001100110011001100110011001100110011001101
    + 1.1001100110011001100110011001100110011001100110011010
    ————————————————————————————————————
    10.0110011001100110011001100110011001100110011001100111

  3. 规格化
    将这个结果处理一下,即结果规格化,变成 1.0011001100110011001100110011001100110011001100110011(1) * 2^-2

  4. 舍入处理
    括号里的 1 是计算后这个 1 超出了范围,也就是超出了 52 位,所以要舍弃。四舍五入对应到二进制中,就是 0 舍 1 入,因为要把括号里的 1 丢了,这里会进 1,结果变成 1.0011001100110011001100110011001100110011001100110100 * 2^-2

    PS:这里不涉及溢出判断。

所以最终的结果存成 64 位就是

0 01111111101 0011001100110011001100110011001100110011001100110100

将它转换为 10 进制数就得到 0.30000000000000004440892098500626

因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3

总结

  1. 小数转二进制可能有些麻烦,需要两边不断乘2取0或1。
  2. 用科学计数法时,我们只需要存储 S、Fraction、E(E+bais)。
  3. 用 64 位表示的二进制中,其中1位表示符号位,11位表示指数位(注意这里是 E+bias),52位表示小数位。

你可能感兴趣的:(前端面试)