探索计算机的小数世界:从二进制到无限精度

前言

不知道你是否和我一样,遇到这个情况。在刚开始学习编程。计算0.3-0.2=0.1这个小学都知道,但是如果你在计算机执行,发现结果并不是0.1 。这个时候会疑问到底是为什么呢。

 System.out.println("0.3-0.2="+(0.3-0.2));
0.3-0.2=0.09999999999999998

对于很多人来说都理解,是因为在计算机中,浮点数使用二进制表示,有限的二进制位数无法精确表示某些十进制小数。那么实际32位的浮点数,在计算机中是如何表示的呢。

二进制小数

在说小数之前,我们先聊下整数。
而说到整数,正整数很简单就是使用原码即可。
在二进制中,我们通常用最高位表示符号位,0 表示正数,1 表示负数。这导致了一个问题,即在进行加法和减法运算时,可能会出现溢出。例如,两个正数相加,结果超出了二进制能表示的范围,就会导致溢出。

了解决这个问题,人们引入了反码表示法。反码是通过将二进制数中的每个位取反(0 变成 1,1 变成 0)得到的。这样,两个正数相加后,如果发生溢出,就会产生一个额外的进位,将其添加到最低位,防止溢出。

然而,反码表示法引入了一个新的问题,即存在两个表示零的形式,即全 0 和全 1。为了解决这个问题,人们进一步引入了补码表示法,通过在反码的基础上再加上 1 来得到。补码表示法只有一个零的表示形式,同时在加法和减法运算中更加方便和一致。因此,补码取代了反码成为计算机中表示有符号整数的主要方式。

    反码(Ones' Complement)反码表示负数时,将其正数部分的二进制表示按位取反。即将0变为1,将1变为0。例如,正数5的二进制表示为"00000101",对应的反码为"11111010"。反码的符号位为1表示负数。反码中存在两个零,即正零和负零,因此在进行加减运算时需要特殊处理 (存在两个表示零的形式,即全 0 和全 1 )补码(Two's Complement)补码表示负数时,将其正数部分的二进制表示按位取反(得到反码),然后再加1。补码的符号位为1表示负数。例如,正数5的二进制表示为"00000101",对应的补码为"11111011"。补码解决了反码的正零和负零问题,并且能够通过简单的加法来实现负数的加减运算。补码理解是把最高位1表示为负数,那么1000 0000=-12811111011=-128+64+32+16+8+2+1=-5移码(又叫增码)是对真值补码的符号位取反,一般用作浮点数的阶码,引入的目的是便于浮点数运算时的对阶操作。-3=1000 0011(原码)=1111 1100(反码)=1111 1101(补码)=0111 1101(移码)3=0000 0011(原码)=0000 0011(反码)=0000 0011(补码)=1000 0011(移码)

在十进制 0.1=1/10,在二进制中0.1=1/2.
那么3/4=0.75(十进制)=0.11(二进制)
因为 这是因为0.75等于1/2 + 1/4,在二进制中分别对应于0.1和0.01,因此0.75的二进制表示为0.11。在这里我们可以看到和十进制类似也就是每多一位 (*1/2)。这就是浮点数表示的基本方法。

但是上面表示有一个问题,例如1000000.1和1.0000000001 我们看到这两个数一个整数很大为,一个小数很多位。那么假设当前浮点数是32位,那么需要如何控制小数点放在哪里呢。遇到1000000.1放在后面一点比较划算,遇到1.0000000001 小数点放在前面比较划算。为了解决这个问题,人们使用IEEE 754表示浮点数。

IEEE二进位浮点数算术标准(IEEE 754)

IEEE 754 是一种用于二进制浮点数算术的标准,它定义了浮点数的表示方式、舍入规则以及基本的算术运算规则。该标准由 IEEE(Institute of Electrical and Electronics Engineers,电气和电子工程师学会)制定,于 1985 年首次发布,后来经过多次修订。

一些常见协会说明
IETF (Internet Engineering Task Force)互联网工程任务组, 成立于1985年,主要定义互联网标准(IP,TCP,HTTP)
IEEE (Institute of Electrical and Electronics Engineers)电气和电子工程师协会, 成立于1961年, 主要致力于电气、电子、计算机工程和与科学有关的领域的开发和研究.(浮点数,局域网)
ANSI (AMERICAN NATIONAL STANDARDS INSTITUTE)美国国家标准学会, 成立于1918年, 实际上成了国家标准化中心(ASCII编码)
ISO (International Organization for Standardization)国际标准化组织, 成立于1926年,主要制订国际标准(ISO9001编码)

IEEE 754 主要定义了两种浮点数表示格式:单精度(32 位)和双精度(64 位)。这两种格式都包括三个部分:符号位、指数部分和尾数部分。

我们先看下我们人类如何表示浮点数:

浮点数(科学计数法):123.45 用十进制科学计数法可以表达为 1.2345 × 10^2 ,尾数1.2345,基数10,指数2。我们先看下,在计算机中是如何表示的。

为了方便计算,接下来的例子都是使用单精度即float类型

123.45=01000010111101101110011001100110

我们可以看到这个和之前整数,表示方式不同。

    单精度:符号(sign)+ 指数(Exponent) + 尾数(Mantissa1bit         8bit         23bit =32()=4字节IEEE浮点标准 V=(-1)^S*M*2^E (e表示在IEEE的表示,E表示实际的指数)

我们再看下 123.45,如果使用科学计数法那么就是1.234510^2 ,而如果是0.045=0.4510^-1
那么用二进制怎么表示,我们看个简单的方式

0.75表示为二进制形式:0.75的二进制表示形式是0.11。根据IEEE 754标准,浮点数表示形式可以写为:(-1)^s * M * 2^E,其中s是符号位(0表示正数,1表示负数),M是尾数(即小数部分的二进制表示形式),E是指数。根据0.75的二进制表示形式,M11。由于0.75是正数,所以符号位s为0。确定指数E:将0.11转换为科学计数法形式,得到0.11 = 1.1 * 2^(-1),所以E-1。因此,0.75IEEE 754表示形式为:符号位 s = 0(表示正数)指数 E = -1(偏移量为127,所以实际指数为127 + (-1) = 126,用127的二进制表示为0111 1110)尾数 M = 1.1,M=100 0000 0000 0000 0000 0000,小数点前面都是1隐藏去那么就剩下1,而M的位数是23位将这些部分组合起来,得到IEEE 754表示为:0 01111101 10000000000000000000000

再看个负数的例子

  -12.5D=-1100.1B=-1.1001*2^(3)=1100 0001 0100 1000 0000 0000 0000 0000  (先转成二进制然后再进行转换操作,不是直接在十进制中进行)S=1;e=3(阶码)=1000 0010;M=100 1000 0000 0000 0000 0000; e=3+127=130=1000 0010(无符号数)
    浮点数不能准确表示0,只能近似的非常小的数表示0。那么+0就是1.0*2^-127e 由 8 位表示,取值范围为 0-255,去除 0255 这两种特殊情况,那么指数 E 的取值范围就是 1-127=-126254-127=12732位中所有位都为0时,表示的就是正零;第一位位1,剩下都是0时表示负0(正0=0000 0000 0000 0000 0000 0000 0000 0000 ;负0=1000 0000 0000 0000 0000 0000 0000 0000)对于32位中符号位为0,指数e部分全为1,尾数M部分全为0时,表示的就是正无穷大。负无穷那么就是符号位变为1 (正无穷=0111 1111 1000 0000 0000 0000 0000 0000;负无穷=1111 1111 1000 0000 0000 0000 0000 0000)最大正数:S=0,阶码e=254,指数E=254-127=127,尾数M=11111111111111111111111。v=(-1)^0*(1.11111111111111111111111)2^1273.402823e+38最小正数:S=0,阶码e=1,指数E=1-127=-126,尾数M=00000000000000000000000. v=(-1)^0*(1.0)*2^-126=1.175494e−38

IEEE小数代码实现

为了方便,查看转换的值。这边将二进制对应十进制的值算出来,看下。

package demo.code.utils;/*** IEEE浮点数表示实现** @author jisl on 2023/11/21 15:42**/
public class IEEEFloatingPointConverter {// 将单精度浮点数转换为二进制字符串public String floatToBinary(float num) {// 将浮点数打包成字节数组int binary = Float.floatToRawIntBits(num);// 将整数转换为二进制字符串String binaryStr = Integer.toBinaryString(binary);// 将二进制字符串填充到32位长度return String.format("%32s", binaryStr).replace(' ', '0');}// 将双精度浮点数转换为二进制字符串public String doubleToBinary(double num) {// 将双精度浮点数打包成字节数组long binary = Double.doubleToRawLongBits(num);// 将长整数转换为二进制字符串String binaryStr = Long.toBinaryString(binary);// 将二进制字符串填充到64位长度return String.format("%64s", binaryStr).replace(' ', '0');}// 将二进制字符串转换为单精度浮点数public double binaryToFloat(String binary) {// 获取符号位int s = Integer.parseInt(binary.substring(0, 1), 2);// 获取指数部分int e = Integer.parseInt(binary.substring(1, 9), 2);// 计算指数int E = e - 127;// 获取尾数部分String M = "1." + binary.substring(9);// 计算浮点数return Math.pow(-1, s) * binaryParseFloat(M, E);}// 辅助方法:将二进制小数字符串转换为浮点数private double binaryParseFloat(String M, int E) {// 如果有小数点,分割整数和小数部分String[] parts = M.split("\\.");int intPart = Integer.parseInt(parts[0], 2);// 左移,以包括小数部分intPart <<= parts[1].length();intPart += Integer.parseInt(parts[1], 2);// 计算浮点数double res = intPart / Math.pow(2, parts[1].length());// 根据指数调整浮点数if (E > 0) {return res * Math.pow(2, E);} else {return res / Math.pow(2, Math.abs(E));}}public static void main(String[] args) {IEEEFloatingPointConverter binaryConverter = new IEEEFloatingPointConverter();String result = binaryConverter.floatToBinary(0.1F);double x = binaryConverter.binaryToFloat(result);System.out.println("单精度浮点数二进制表示:" + result);System.out.println("转换回浮点数:" + x);System.out.println("0.3-0.2="+(0.3-0.2));}
}

读者可以执行上面代码,查看对应二进制值转成十进制。这也就是出现0.3-0.2=0.1原因。

BigDecimal介绍

从上面我们知道,如同在十进制中无法精确表示1/3,在二进制中我们也无法精确表示1/5。但是在实际生活中,我们不可避免要使用1/5这个精确小数。为了满足这个需要,大家用了个方法。就是二进制整数都是精确,那么我就先把你改成整数,进行运算。那么不就是精确了,这个方式和现在后端很多金额都是用分计算,思路是一样的。

BigDecimal是Java中的一个类,用于精确表示任意精度的十进制数。它的原理基于一个不可变的任意精度整数,它可以存储任意大小的整数,而不会丢失精度。

我们看下BigDecimal构成

public class BigDecimal extends Number implements Comparable<BigDecimal> {/*** The unscaled value of this BigDecimal, as returned by {@link* #unscaledValue}.** @serial* @see #unscaledValue*/private final BigInteger intVal;/*** The scale of this BigDecimal, as returned by {@link #scale}.** @serial* @see #scale*/private final int scale;  // Note: this may have any value, so// calculations must be done in longs/*** The number of decimal digits in this BigDecimal, or 0 if the* number of digits are not known (lookaside information).  If* nonzero, the value is guaranteed correct.  Use the precision()* method to obtain and set the value if it might be 0.  This* field is mutable until set nonzero.** @since  1.5*/private transient int precision;/*** Used to store the canonical string representation, if computed.*/private transient String stringCache;/*** Sentinel value for {@link #intCompact} indicating the* significand information is only available from {@code intVal}.*/static final long INFLATED = Long.MIN_VALUE;private static final BigInteger INFLATED_BIGINT = BigInteger.valueOf(INFLATED);/*** If the absolute value of the significand of this BigDecimal is* less than or equal to {@code Long.MAX_VALUE}, the value can be* compactly stored in this field and used in computations.*/private final transient long intCompact; 
}               
        final BigDecimal bigDecimal = new BigDecimal("0.3");

在这里插入图片描述

我们通过 0.3列子 解释以上几个值。

intVal:intVal是一个BigInteger对象,用于存储BigDecimal对象表示的整数部分的值。当整数部分可以用long类型表示时,intVal可能为空(null),而直接使用long类型的intCompact字段表示整数部分的值。
scale:scale是一个int类型的值,表示BigDecimal对象的小数部分的精度,即小数点右边的位数。正数表示小数点右边的位数,负数表示小数点左边的位数。例如,一个BigDecimal对象的scale为2表示其小数部分有两位小数。
stringCache:stringCache是一个String类型的缓存,用于缓存BigDecimal对象的字符串表示形式。当调用toString()方法时,会将结果缓存到stringCache中,以提高性能。
intCompact:intCompact是一个long类型的字段,用于存储BigDecimal对象的整数部分的值,当整数部分可以用long类型表示时,使用该字段来存储整数部分的值,以提高性能和减少内存占用。如果整数部分的值不能用long类型表示,则intCompact字段为0,而整数部分的值则存储在intVal字段中的BigInteger对象中。
例如    final BigDecimal bigDecimal = new BigDecimal("0.3");
变成整数,那么 stringCache="0.3",intCompact=3,scale=1(移动一位就变成整数了),intVal=null(因为intCompact可以表示)

BigDecimal运算

我们看下BigDecimal是如何计算的

/*** 执行两个BigDecimal数的加法运算。* @param xs 第一个数的整数部分* @param scale1 第一个数的小数点位数* @param ys 第二个数的整数部分* @param scale2 第二个数的小数点位数* @return 加法运算的结果*/
private static BigDecimal add(final long xs, int scale1, final long ys, int scale2) {// 计算两个数的小数点位数之差long sdiff = (long) scale1 - scale2;if (sdiff == 0) {// 如果两个数的小数点位数相同,则直接调用另一个重载的add方法执行加法运算return add(xs, ys, scale1);} else if (sdiff < 0) {// 如果第一个数的小数点位数较小,则将其扩大到与第二个数相同的位数,然后执行加法运算int raise = checkScale(xs, -sdiff);// 将第一个数的整数部分扩大到与第二个数的小数点位数相同long scaledX = longMultiplyPowerTen(xs, raise);if (scaledX != INFLATED) {// 如果扩大后的结果可以用long类型表示,则直接执行加法运算return add(scaledX, ys, scale2);} else {// 如果扩大后的结果不能用long类型表示,则使用BigInteger进行加法运算BigInteger bigsum = bigMultiplyPowerTen(xs, raise).add(ys);// 根据加法结果的符号创建新的BigDecimal对象return ((xs ^ ys) >= 0) ? // 判断加法结果的符号是否相同new BigDecimal(bigsum, INFLATED, scale2, 0): valueOf(bigsum, scale2, 0);}} else {// 如果第二个数的小数点位数较小,则将其扩大到与第一个数相同的位数,然后执行加法运算int raise = checkScale(ys, sdiff);// 将第二个数的整数部分扩大到与第一个数的小数点位数相同long scaledY = longMultiplyPowerTen(ys, raise);if (scaledY != INFLATED) {// 如果扩大后的结果可以用long类型表示,则直接执行加法运算return add(xs, scaledY, scale1);} else {// 如果扩大后的结果不能用long类型表示,则使用BigInteger进行加法运算BigInteger bigsum = bigMultiplyPowerTen(ys, raise).add(xs);// 根据加法结果的符号创建新的BigDecimal对象return ((xs ^ ys) >= 0) ?new BigDecimal(bigsum, INFLATED, scale1, 0): valueOf(bigsum, scale1, 0);}}
}

从上面我们可以看到,加法运算变成整数和整型是一样。就是转换后如果scale不同,那么需要转换。我们看下

        final BigDecimal bigDecimal = new BigDecimal("0.3");final BigDecimal bigDecimal2 = new BigDecimal("0.12");System.out.println(bigDecimal.add(bigDecimal2));0.3是intCompact=3,scale=10.2变成intCompact=12,scale=2两个scale不一样,改变0.3的字段值 intCompact=30,sacle=2开始进行加法运算:30+12=42,而scale=2,那么就是0.42

在这里插入图片描述

总结

以上介绍了二进制小数表示,然后介绍了IEEE浮点数表示法,最后介绍了BigDecimal如何解决二进制不能精确表示十进制小数问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/808031.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

API管理平台:你用的到底是哪个?

Apifox是不开源的&#xff0c;在github的项目只是readme文件&#xff0c;私有化需要付费。当然saas版目前是免费使用的。 一、Swagger 为了让Swagger界面更加美观&#xff0c;有一些项目可以帮助你实现这一目标。以下是一些流行的项目&#xff0c;它们提供了增强的UI和额外的功…

OSCP靶场-- Sybaris

OSCP靶场–Sybaris 考点(redis MODULE LOAD命令执行) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.158.93 -sV -sC -Pn --min-rate 2500 -p- Starting Nmap 7.92 ( https://nmap.org ) at 2024-04-11 04:24 EDT Nmap scan report for 192.168.158.93…

照片转漫画的软件有吗?分享4款热门的软件!

在数字化时代&#xff0c;我们总是追求新鲜、有趣、创意十足的方式来展现自我。其中&#xff0c;将普通照片转化为漫画风格的图像已成为许多年轻人的新宠。这种既能保留原照片中的人物特征&#xff0c;又能赋予其独特艺术气息的方式&#xff0c;让许多人趋之若鹜。那么&#xf…

PHP7垃圾回收算法

前提 本文为了梳理PHP GC工作流程&#xff0c;所以从引用计数、部分标记清除算法做引子&#xff0c;然后介绍PHP GC工作流程,最后介绍性能更高的GC算法 引用计数 概述 引用计数算法中引入了一个概念计数器。计数器代表对象被引用的次数 基本原理 为了记录一个对象有没有被…

微信公众号第三方平台-公众号扫码授权接入代运营

文章目录 接入目的效果展示技术积累如何成为服务商如何搭建第三方后端服务传统模式V云服务模式如何完成商家授权授权逻辑介绍 环境准备注册开发者平台-个人类型 传统模式后端代码接收公众号个人三方平台的票据根据票据获取三方平台访问令牌根据访问令牌获取预授权码通过预授权码…

OJ 【难度1】【Python】完美字符串 扫雷 A-B数对 赛前准备 【C】精密计时

完美字符串 题目描述 你可能见过下面这一句英文&#xff1a; "The quick brown fox jumps over the lazy dog." 短短的一句话就包含了所有 2626 个英文字母&#xff01;因此这句话广泛地用于字体效果的展示。更短的还有&#xff1a; "The five boxing wizards…

网络——初识网络

在现如今&#xff0c;网络已经成了一种基础设施&#xff0c;大到国家&#xff0c;小到个人&#xff0c;网络已经充斥在我们每个人的身 边&#xff0c;如果一个人突然失去了网络&#xff0c;那么它的生活或多或少会出现一些不方便的地方&#xff0c;网络现在已 经伴随着我们的吃…

Solana主网使用自定义的RPC进行转账

1、引言 如果用 browser 连接主网的 RPC server 会收到 error code 403 message 為 Access forbidden, contact your app developer or supportrpcpool.com. 错误&#xff0c;因为主网的 RPC server 会检查 HTTP Header 如果判断出來是 browser 就会报告 403 錯誤。 要解決这…

N 皇后 - 蓝桥杯?-Lua 中文代码解题第6题

n 皇后问题 研究的是如何将 n 个皇后放置在 n n 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回 n 皇后问题 不同的解决方案的数量。 示例 1&#xff1a; 输入&#xff1a;n 4 输出&#xff1a;2 解释&#xff1a;如上图所示&…

吴恩达2022机器学习专项课程(一) 5.2 向量化(1) 5.3 向量化(2)

问题预览/关键词 什么是向量化&#xff1f;向量化的好处是&#xff1f;如何向量化多元线性回归函数的参数&#xff1f;如何在Python中向量化参数&#xff1f;计算机底层是如何计算向量化的&#xff1f;向量化示例 笔记 1.向量化 一种在数学和计算中广泛使用的概念&#xff…

[Mac]安装App后“XX已损坏,无法打开“

问题&#xff1a; “xx.app”已损坏&#xff0c;无法打开。你应该将它移到废纸篓。 解决&#xff1a; 终端输入sudo xattr -r -d com.apple.quarantine 后将Applications中对应的问题app拖入生成路径&#xff0c;然后执行。 $ sudo xattr -r -d com.apple.quarantine /Appli…

备战蓝桥杯(日益更新)(刷题)

备战蓝桥杯&#xff08;日益更新&#xff09;&#xff08;刷题&#xff09; 文章目录 备战蓝桥杯&#xff08;日益更新&#xff09;&#xff08;刷题&#xff09;前言&#xff1a;一、二分&#xff1a;1. acwing503 借教室&#xff1a;&#xff08;二分 差分&#xff09;2. ac…

Socks5代理IP如何使用?详细教程解析

当我们在互联网上浏览网页、下载文件或者进行在线活动时&#xff0c;隐私和安全问题常常被提及。在这样的环境下&#xff0c;一个有效的解决方案是使用Sock5IP。本教程将向您介绍Sock5IP的使用方法&#xff0c;帮助您保护个人隐私并提升网络安全。 一、什么是Sock5IP&#xff1…

Mybatis-Plus使用入门

Mybatis-Plus 一、Mybatis-plus的简介 官方文档的地址&#xff1a; MyBatis-Plus &#xff08;一&#xff09;什么是Mybatis-Plus Mybatis-Plus是一个Mybatis&#xff08;opens new window&#xff09;的增强工具&#xff0c;在Mybatis的基础上只做增强不做改变&#xff0c…

ChatGPT在日常生活与工作中的应用,以及Hulu AI 的探索之旅

ChatGPT在日常生活与工作中的应用&#xff0c;以及Hulu AI 的探索之旅 &#x1f4ac;ChatGPT 的多面应用&#x1f4ac;Hulu AI&#xff1a;一个AI工具聚合平台的探索平台优势为何选择Hulu AI&#xff1f;珍稀优惠 &#x1f4ac;结束语 在数字化快速发展的当下&#xff0c;人工智…

冯喜运:4.11外汇黄金原油晚间行情分析及独家作家操作建议

【 黄金消息面分析】&#xff1a;周四(4月11日)亚市早盘&#xff0c;现货黄金窄幅震荡&#xff0c;周三金价从纪录高位下滑&#xff0c;盘中一度失守2320关口至2319.一线&#xff0c;收报2333附近&#xff0c;因此前强于预期的通胀数据削弱了美国提前降息的预期&#xff0c;美元…

Python初级第二次作业

一、 def reverse(num):anumt0b0cnumwhile a//10>0:if a%10>0:t1aa//10print(t)for i in range(t,-1,-1):if c%100:b0else:b(c%10)*(10**i)c//10print(b) if bnum:return Trueelse:return Falsedef isPalind(num):kreverse(num)if kTrue:print(f"{num}是回文&…

uniapp 轮播列表一排展示3个,左右滑动,滑动到中间放大

一、效果展示 二、代码实现 1.html代码&#xff1a; <!-- 轮播 --><view class"heade"><swiper class"swiper" display-multiple-items3 circulartrue previous-margin1rpx next-margin1rpxcurrent0 change"swiperChange">&l…

书生·浦语2.0(InternLM2)大模型实战--Day02 茴香豆 | 搭建RAG智能助理

视频地址&#xff1a;https://www.bilibili.com/video/BV1QA4m1F7t4/文档地址&#xff1a;https://github.com/InternLM/Tutorial/blob/camp2/huixiangdou/readme.md作业地址&#xff1a;https://github.com/InternLM/Tutorial/blob/camp2/huixiangdou/homework.md RAG 概述 R…

汇舟问卷:海外问卷怎么做?

最近美元升值了&#xff0c;但是想在国内赚取美金的途径很少&#xff0c;大多数人接触不到赚取美金的机会。目前汇舟问卷做的国外问卷调查就是一个赚取美金的机会。 操作步骤也比较简单&#xff0c;只需要先搭建好国外的ip环境&#xff0c;然后创建对应国家的人设&#xff0c;…