flag
mode_edit

神秘的浮点数舍入

printf 输出浮点数的时候,究竟是怎么舍入的呢?

有一种比较常见的误解,是认为它使用了四舍五入。

考虑下面的代码的输出:

#include <bits/stdc++.h>

using namespace std;

void round_to_1(double a) {
  printf("%lf => %.1lf\n", a, a);
}
void round_to_0(double a) {
  printf("%lf => %.0lf\n", a, a);
}

int main () {
  for (int i = 1; i < 9; ++i) {
    round_to_0(1 * i + 0.5);
  }
  std::cout << "===========" << std::endl;
  for (int i = 1; i < 9; ++i) {
    round_to_1(0.1 * i + 0.05);
  }
}

输出(删去了多余的0):

1.5 => 2
2.5 => 2
3.5 => 4
4.5 => 4
5.5 => 6
6.5 => 6
7.5 => 8
8.5 => 8
===========
0.15 => 0.2
0.25 => 0.2
0.35 => 0.4
0.45 => 0.5
0.55 => 0.6
0.65 => 0.7
0.75 => 0.8
0.85 => 0.9

太玄妙了。

向偶数取整/奇进偶舍/四舍六入五成双

首先来研究等号以上的部分。这时候使用的舍入方法并非常见的四舍五入。而是一种比较奇妙的舍入方式:

也称银行家舍入法。按照一般的规则进行四舍六入,如果下一位正好为 5,并且之后没有更多的位数,则舍入至双数。例如,3.5 舍入成 4,而 6.5 舍入成 6。在四舍五入的数据比较多的情况下,可以避免平均数等统计数据出现较大误差。

好,那么我们来继续看等号以下的部分:好像不大对劲!

如果按照奇进偶舍的规律,0.45 应该舍入到 0.4,而 0.65 应该舍入到 0.6,而 0.85 应该是 0.8 才对!

浮点误差

实际上浮点数能精确表示的值并不多。正如能表示成有限位十进制小数的有理数 $a$ 都具有 $a = \dfrac{p}{2^{q_1}\cdot5^{q_2}}$ 的形式一样,有限位二进制小数能准确表示的有理数都具有 $a=\dfrac p{2^q}$ 的形式。我们来以 0.45 为例,看一下究竟是怎么回事:

0.45 表示为 32 位的浮点数时,是 $2^{-2}\cdot 1.7999999523162841796875\approx0.4499999880791$,而表示为 64 位的浮点数时,是 $2^{-2}\cdot 1.8000000000000000444089209850062616169452 \geq\dfrac{1.8}4=0.45$, 因此 0.45 实际上是大于 0.45 的,因此应用奇进偶舍的时候时会变成 0.5.

考虑到 0.45 的奇妙性质,因此下面的代码的输出是 0.4 0.5

printf("%.1f %.1lf", (float)0.45, 0.45)

Reference