完全解析为什么0.1+0.2=0.30000000000000004

阅读全文根据基础需要20-60分钟

引子

可能因为某一次报错或某次面试,你会接触到这个问题,即
在这里插入图片描述
其实该问题很多语言都存在,你也可以在https://0.30000000000000004.com/了解其他信息,本文主要以Java为例对该问题进行讨论

计算结果多样原因

如果你已经参阅了好几篇讲解0.1+0.2为什么!=0.3的文章,你会发现大家在浮点数的讲解上大同小异,不过最终0.1+0.2的结果却大体分为4种不同的结果,这里的原因是

  • 有些博主以float(单精度浮点数)举例
  • 有些博主以double(双精度浮点数)举例

同时在计算过程中,博主们的计算过程大致为为两类

  • 将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制
  • 将0.1与0.2转为实际的二进制浮点数 → 将浮点数转回十进制 → 直接进行十进制数计算

所以大家会看到4种不同的计算结果,如果除此4种结果以外还存在其他结果,那么多半是由于粗心在某些步骤出现了计算失误,本篇会带着大家逐步完成其中3种情况的计算,最后一种留作习题用以检验自己的学习成果

需要注意的是,Java本身的计算顺序是
将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制
所以其他3种情况的结果都不和0.30000000000000004相符

正文

Java中的默认数据类型

我们首先需要知道,Java中的整数默认是int型,小数默认是Double
所以我们可以理解为这是Double型的0.1与0.2相加

到底什么是Double?

Double是符合IEEE_754-1985计算机浮点数标准的双精度浮点数,这一标准也规定了浮点数要以什么样的形式保存在计算机中

10进制转换为浮点数

在 IEEE_754-1985 规范中规定了浮点数的二进制表现形式以及长度限制
长度限制如下

数据类型定义长度
float单精度浮点数32位2进制
double双精度浮点数64位2进制

浮点数的表现形式分为3部分,以上题需求,这里以double为例
在这里插入图片描述

  • 第一部分为符号位,仅占用1位(bit)——0为正,1为负
  • 第二部分为指数位,占用11位,用来存储经偏指数进行计算过后的指数
  • 第三部分为分数部分,占用52位,用来存储分数,分数长度不够后方全部补0直到长度符合要求

下面我们以十进制数0.15625为例,逐步计算将其转为双精度浮点数,来让大家实际了解十进制数如何转为双精度浮点数

符号位

0.15625 很明显是正数,所以符号位为0

指数位

这里我们需要先将十进制数0.15625转为二进制数,即为0.00101

如果你对2进制不了解,可以百度搜索进制转换工具帮助完成这一步,这里不做进制转换工具推荐

之后我们需要以科学计数法的形式表示0.00101,即为1.01 * 2 指数为-3

科学计数法:
把一个数表示成a与10的n次幂相乘的形式(1≤|a|<10,a不为分数形式,n为整数
举例
12的科学计数法表示为:1.2 * 10 指数为1
在二进制中即为把一个数表示成a与2的n次幂相乘的形式(1≤|a|<2,a不为分数形式,n为整数

同时浮点数标准也为不同精度的浮点数规定了固定的指数位偏差,双精度浮点数(double)的固定偏差为1023
此时计算指数与偏差,即(1023) + (-3) = 1020,将1020转为二进制,即为1111111100
此时我们回看指数位的定义,double的指数位长度为11位,(1111111100)长度为10位,所以我们需要在数前补0直到长度为11位,即将1111111100变换为01111111100
所以指数位为 01111111100

分数位

回看上一步得到的科学计数法的表示——1.01 * 2 指数为-3
分数部分即为小数部分 即为1.01中的01,不过由于01的位数太少,我们需要在其后方补0直到满足分数位52位的要求
结果为:0100000000000000000000000000000000000000000000000000
最终进行拼接,即0.15625的双精度浮点数表示为
0011111111000100000000000000000000000000000000000000000000000000

浮点数转换为10进制

大家已经会了10进制如何转为浮点数了,那么大部分人应该已经能推导出浮点数要如何转回10进制了,不过这里依然还是要讲一下
还是以
0011111111000100000000000000000000000000000000000000000000000000为例
首先区分3部分
0 01111111100 0100000000000000000000000000000000000000000000000000
符号位可以确定数为正数,指数位部分2进制转10进制,为1020,double偏指数为1023
1020 - 1023 = -3则指数为-3
分数部分可以简写为01,需要注意的是,这里隐含的前导数固定为1,即可以知道原数为1.01
此时进行计算1.01 * 2 指数为-3,得出结果为0.00101,最后直接2进制转回10进制即可

Double——情况1讨论

这里的情况为,将小数类型以Double处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制 进行处理

0.1的二进制浮点数

0.1的二进制浮点数为
0011111110111001100110011001100110011001100110011001100110011010
如果你以上方讲解进行转换,会遇到无限循环小数问题,即0.1转为2进制是无限循环小数,这时大家得出的结果应该都为
0011111110111001100110011001100110011001100110011001100110011001
那么为什么这里我的结果和大家不一样呢?因为当分数部分位数不够,即出现分数丢失精度情况时,我们需要做一个操作——舍入
浮点数的默认舍入方式和大家平时理解的四舍五入基本一致,在本情况下直接将最后一位1向前进位1即可

如果你希望对浮点数的舍入进行更多了解,可以自行百度
0.2的二进制浮点数

0.2的二进制浮点数为
0011111111001001100110011001100110011001100110011001100110011010
0.2同样存在舍入操作

浮点数计算

此时我们需要计算
0.1 → 0011111110111001100110011001100110011001100110011001100110011010
+
0.2 → 0011111111001001100110011001100110011001100110011001100110011010
的结果,不过浮点数运算和常规运算不同,我们一步一步进行

对阶——可能影响数据准确

浮点数计算的第一步为对阶,即将参与计算的浮点数的指数位保持一致
需要注意,对阶的原则为小阶向大阶对阶
这里我们首先计算他们的阶码
0.1阶码 → 01111111011 → 1019
0.2阶码 → 01111111100 → 1020
所以我们这里需要做的就是将0.1的阶码+1变为1020
01111111011 → 01111111100
因为阶码的改变,为了不让浮点数表示的数发生改变,分数部分需要向右同步移1位,这里需要注意,分数部分隐含前导数为1,即右移的第一位进来的数是1

如果这一步你没看懂,可以以科学计数法的形式进行思考,即
12 → 1.2 * 10 阶码为1
此时需要将阶码改为2,则分数部分需要进行变动,最终表示变为
0.12 * 10 阶码为2

1001100110011001100110011001100110011001100110011010 变为
1100110011001100110011001100110011001100110011001101
对阶结束时,0.1和0.2的值分别为
0.1 → 0011111111001100110011001100110011001100110011001100110011001101
0.2 → 0011111111001001100110011001100110011001100110011001100110011010

尾数计算(分数计算)

浮点数计算的第二步为直接计算浮点数的第三部分,这里需要注意不要丢失分数部分隐含的前导数
0.1 → 0.1100110011001100110011001100110011001100110011001101
+
0.2 → 1.1001100110011001100110011001100110011001100110011010
=
0.3 →10.0110011001100110011001100110011001100110011001100111
尾数部分计算到这里就结束了,我们接着下一步处理

这里0.1的前导数为0是因为分数部分在对阶的阶段已经右移了1位所以隐含前导数1已经移动到了分数部分的开头
规格化处理——可能影响数据准确

IEEE 754-1985标准要求浮点数的隐含前导数必须为1,但是此时计算结果为
10.0110011001100110011001100110011001100110011001100111
隐含前导数为10,所以我们需要进行规格化处理,让前导数重新变为1
这里我们需要执行的操作为右规,即将分数部分向右移动一位
10.0110011001100110011001100110011001100110011001100111 变为
1.0011001100110011001100110011001100110011001100110011
需要注意,这里最后一位的1被丢掉了,从而会影响数据准确
规格化处理后的分数部分为
0011001100110011001100110011001100110011001100110011
因为分数部分向右移动了一位,所以阶码需要+1
01111111100 → 01111111101

规格化处理分为左规和右规,左规即将分数部分向左移动,右规即将分数部分向右移动
舍入操作

由于规格化处理时导致低位丢失,所以需要进行舍入操作,即
0011001100110011001100110011001100110011001100110011 变为
0011001100110011001100110011001100110011001100110100

溢出判断

这里没有溢出

最终计算结果即为
符号位 0
指数位 01111111101
分数 0011001100110011001100110011001100110011001100110011

结果转为10进制

01111111101 代表指数为1021 带入偏差1023进行计算结果为-2
所以分数小数点需向左移动2位
0011001100110011001100110011001100110011001100110011 变为
0.010011001100110011001100110011001100110011001100110011
转为10进制为:3.00000000000000044408920985006E-1
即 0.300000000000000044408920985006
这就是为什么0.1+0.2=0.30000000000000004的原因

Double——情况2讨论

这里的情况为,将小数类型以Double处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 将浮点数转回十进制 → 直接进行十进制数计算 进行处理
0.1和0.2的浮点数可以在上种情况中直接拿来使用
0.1 → 0011111110111001100110011001100110011001100110011001100110011010
0.2 → 0011111111001001100110011001100110011001100110011001100110011010

浮点数转回10进制

0011111110111001100110011001100110011001100110011001100110011010 → 0.100000000000000005551115123126
0011111111001001100110011001100110011001100110011001100110011010 → 0.200000000000000011102230246252

10进制计算

0.100000000000000005551115123126
0.200000000000000011102230246252
0.300000000000000016653345369378
因此最终结果为0.300000000000000016653345369378

Float——情况1讨论

这里的情况是,将小数类型以Float处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 将浮点数转回十进制 → 直接进行十进制数计算 进行处理
0.1 → 00111101110011001100110011001101
0.2 → 00111110010011001100110011001101

浮点数转回十进制

00111101110011001100110011001101 → 0.100000001490116119384765625
00111110010011001100110011001101 → 0.20000000298023223876953125

10进制计算

0.100000001490116119384765625
0.20000000298023223876953125
0.300000004470348358154296975
因此结果为0.300000004470348358154296975

到此为止我说的4种情况其中的3种都进行逐步计算,最后一种的情况为

将小数类型以Float处理,计算过程以
将0.1与0.2转为实际的二进制浮点数 → 进行二进制浮点数计算 → 计算结果转为十进制 进行处理

这种情况留作检验阅读本博客的作业,大家可以自己尝试计算一下

参考文章

  1. IEEE 754-1985
  2. Floating Point Math
  3. 浮点数转换工具网站

最后更新于2022年3月14日
原创不易,如果该文章对你有所帮助,望左上角点击关注~如有任何技术相关问题,可通过评论联系我讨论,我会在力所能及之内进行相应回复以及开单章解决该问题.

该文章如有任何错误请在评论中指出,感激不尽,转载请附出处!
个人博客首页:https://blog.csdn.net/yjrguxing ——您的每个关注和评论都对我意义重大

<think>好的,用户让我介绍一下这行代码:x = np.arange(-7, 7, 0.01)。首先,我得确认用户对numpy的了解程度。可能他们刚开始学Python,或者对numpy的函数不太熟悉。 首先,np.arange是numpy里的一个常用函数,用来生成等差数列。参数是起始点、终止点、步长。那这行代码的具体作用呢?起始-7,终止7,步长0.01。生成的数组应该从-7开始,每次加0.01,直到接近7但不包括7。 接下来需要解释每个参数的意义。用户可能想知道为什么终止点是7而不是包含进去,这时候要说明arange函数是左闭右开的。比如,当步长是1的时候,np.arange(0,3)会生成0,1,2,所以同样的道理,这里到7的时候就不会包含进去。 然后,生成的数据点数量是多少呢?计算的话,总长度是14(从-7到7的距离),步长0.01,所以大概有14 / 0.01 = 1400个点,但实际可能因为浮点数精度问题稍微有点差异,但大致是1400个点左右。这点可能需要提到,但用户可能更关心应用场景。 用户可能的应用场景是什么?通常这种密集的点用来画函数图像,比如在matplotlib里画sin、cos或者其他连续函数的图形。比如,用户可能在做数据可视化,或者需要生成一个平滑曲线时需要密集的点。 另外,步长0.01意味着高分辨率,这样画出来的图更平滑。如果步长太大,比如0.1,图像可能会有锯齿感。这里0.01的选择可能是为了平衡精度和计算资源,毕竟太多点会占用内存,但现在的计算机处理1400个点应该没问题。 还要注意,numpy的arange和Python内置的range函数不同,arange可以处理浮点数步长,而range只能处理整数。这时候可能需要比较两者的区别,帮助用户理解为什么用numpy而不是原生的方法。 可能用户会遇到的问题包括:生成的数组是否正确,是否包含终点,步长是否合适。比如,如果用户想要包括7的话,可能需要调整终止点或者使用其他函数如linspace,这时候可以提到np.linspace( -7, 7, 1401 )会更精确地包括终点,因为可以指定点的数量。 此外,生成的x数组类型是numpy的ndarray,用户可能需要了解后续如何处理这个数组,比如进行数学运算、绘图等。比如,他们可能会用x来计算y = np.sin(x),然后用matplotlib画图。 最后,是否需要给出一个示例?比如展示如何用这个x数组生成一个sin曲线,并绘图。这样用户能更直观地理解这行代码的用途。不过用户的问题只是介绍这行代码,可能不需要深入,但举例子总是有帮助的。 总结下来,回答的结构应该包括函数的作用、参数解释、生成的数据特点、应用场景、可能的注意事项或替代方案,以及示例用法。这样用户能全面了解这行代码的意义和用法。</think>以下是对代码`x = np.arange(-7, 7, 0.01)`的详细介绍: ### 功能说明 使用NumPy库生成一个**等间隔数值序列**,范围从-7到7(不包含7),步长为0.01。这是科学计算中常用的**坐标点生成方法**,特别适合用于函数绘图、信号处理等场景。 --- ### 参数解析 ```python np.arange(start, stop, step) ``` | 参数 | 值 | 说明 | |-------|------|-------------------------| | start | -7 | 起始值(包含) | | stop | 7 | 终止值(不包含) | | step | 0.01 | 步长(相邻两数的间隔) | --- ### 生成数据特点 1. **元素总数**:$\frac{7 - (-7)}{0.01} = 1400$个(实际输出1400浮点数2. **首尾元素**: ```python x[0] = -7.0 # 第一个元素 x[-1] = 6.99 # 最后一个元素(7 - 0.01) ``` 3. **数据类型**:`numpy.ndarray`(NumPy数组) --- ### 数学表达式 生成的数组等价于: $$ x = \{-7.00,\ -6.99,\ -6.98,\ ...,\ 6.98,\ 6.99\} $$ --- ### 典型应用场景 1. **函数绘图**(需配合matplotlib): ```python import matplotlib.pyplot as plt y = np.sin(x) # 计算正弦函数值 plt.plot(x, y) # 绘制平滑曲线 ``` ![正弦函数图](https://example.com/sine-wave.png) 2. **数值模拟** ```python # 高斯分布计算 sigma = 1.0 gaussian = 1/(sigma*np.sqrt(2*np.pi)) * np.exp(-x**2/(2*sigma**2)) ``` 3. **信号处理** ```python # 生成时域信号 t = x # 时间轴 frequency = 2 # 2Hz signal = np.sin(2 * np.pi * frequency * t) ``` --- ### 扩展知识 1. **与`np.linspace`的区别**: - `arange`:通过步长控制生成元素 - `linspace`:通过指定元素数量自动计算步长 ```python # 等效写法(包含终点) x_lin = np.linspace(-7, 7, 1401) # 1401个点 ``` 2. **精度问题**: ```python # 浮点数运算可能导致的误差示例 >>> 0.1 + 0.2 == 0.3 False # 实际值为0.30000000000000004 ``` 因此实际生成的数组末尾可能略微偏离理论值 --- ### 实际输出示例 ```python import numpy as np x = np.arange(-7, 7, 0.01) print(x.shape) # 输出:(1400,) print(x[:3]) # 输出:[-7. -6.99 -6.98] print(x[-3:]) # 输出:[6.97 6.98 6.99] ```
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值