浮点数的精度问题

1. 两个浮点数是否可以直接比较大小?

  • 直接比较的可行性

    大小比较(>、<、>=、<=):可以直接使用,因为浮点数的有序性保证了大小关系的正确性。

    示例3.14 > 2.71a < b 的运算结果是可靠的。

  • 相等性比较(==)的风险

    精度陷阱:浮点数的二进制存储可能导致微小误差,例如:

    1
    0.1 + 0.2 == 0.3  // 在多数语言中返回 False(实际值是 0.30000000000000004)

我们可以看到,两个浮点数由于存在精度问题,所以是无法直接判断是否相等的,那么这是为什么呢,又应该如何解决呢?

2. 浮点数出现精度问题的原因:

浮点数(例如 double 类型)在计算机中是按照 IEEE 754 标准存储的。这种表示方式由三个部分组成:

  1. 符号位(Sign bit):1 位,用于表示正负数。0 表示正数,1 表示负数。
  2. 指数部分(Exponent):用于表示数值的大小范围。对于 double 类型,占用 11 位。
  3. 尾数部分(Mantissa or Fraction):用于表示有效数字(精度部分),double 类型占用 52 位。

由于十进制小数通过乘二取整法转换成二进制小数后往往是一个无限循环小数,但是计算机存储的尾数部分长度是有限的,所以就会对无限循环小数进行截断,我们在使用的时候,使用的是截断后的二进制数据转换成十进制后的数据 ,这样就导致了误差的产生。下面是具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义三个double类型的浮点数a, b, c	
double a = 0.1; // 0.10000000149011611938476562
double b = 0.2; // 0.20000000298023223876953125
double c = 0.3; // 0.29999999999999998889776975

// 直接输出a,b,c的值
System.out.println(a); // 0.1
System.out.println(b); // 0.2
System.out.println(c); // 0.3

// 直接打印出来的值没有精度问题,但是如果我们使用这三个变量进行浮点数计算,就会发现出现了精度问题
System.out.println(a + b); // 0.30000000000000004
System.out.println(a + b - c); // 5.551115123125783E-17

// 所以当我们使用浮点数直接和0比较大小,或者通过和0比较判断正负时,运行的结果和真实的结果有误差
System.out.println((a + b - c) > 0); // 实际的结果为false,但是代码的输出确实true

3. 浮点数精度问题的解决方法:

想要解决浮点数的精度问题,首先要规定一个误差精度,当误差的值比这个精度小的话,就认为没有误差,误差的精度需要根据具体的业务进行设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 设置精度
double epsilon = 1e-16;
double a = 0.1;
double b = 0.2;
double c = 0.3;

System.out.println(epsilon);
System.out.println(a + b - c); // 5.551115123125783E-17

if (Math.abs(a + b - c) < epsilon) {
System.out.println("a + b - c 和 0 相等");
} else {
System.out.println("a + b - c 和 0 不相等");
}
// 执行这段代码输出的结果为:a + b - c 和 0 相等,但是当我们把精度调成epsilon = 1e-17的时候,这个结果就会变成a + b - c 和 0 不相等,所以精度的选择是非常重要的,一定要保证业务中浮点数的计算结果不会超过这个精度

常见问题的解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 假设业务中的浮点数计算的误差精度不会超过1e-8,浮点数类型按double计算
double epsilon = 1e-8;
// 这个temp是业务中通过计算后得到的,这里就不指定具体数值了
double temp;

// 判断浮点数是否等于0
if (Math.abs(temp) < epsilon) {
System.out.println("浮点数:" + temp + "和0相等");
}

// 判断浮点数和0不相等
if (Math.abs(temp) >= epsilon) {
System.out.println("浮点数:" + temp + "和0不相等");
}

// 判断浮点数大于0
if (temp > epsilon) {
System.out.println("浮点数:" + temp + "大于0");
}

// 判断浮点数小于0
if (Math.abs(temp) > epsilon && temp < epsilon) {
System.out.println("浮点数:" + temp + "小于0");
}

// 判断两个浮点数的大小关系,只需要让这两个浮点数做差,得到一个新的浮点数,然后再和0比较大小即可

可以直接比较大小的情况:如果是以下的两种情况,两个浮点数可以直接比较大小,精度误差不影响最终结果

  • 如果比较大小的两个浮点数都是直接定义的或者直接从数据库获取,并且都没有参数过浮点数运算
  • 非常明确这两个浮点数不会存在相等的情况。

4. 精度问题总结:

如果想要使用浮点数比较大小的话有两种简单的方法,一种方法是使用round函数,把浮点数的精度限定在一个规定的精度内,在这个精度内进行比较大小;一种方法是定义一个精度epsilon,通过让浮点数和epsilon进行比较来判断浮点数的正负或者两个浮点数的大小关系。