Perl - while循环的意外结果

时间:2017-09-04 17:50:20

标签: perl while-loop floating-point

这是一个简单的程序,但我无法理解场景后面的while循环所做的逻辑/工作。

问题:编写一个程序,打印从0到1的每个数字,在小数位后面有一个数字(即0.1,0.2等)。

所以这是我的代码:

$num = 0;
while ( $num < 1 ) {
  print "$num \n";
  $num = $num + 0.1;
}

如果我以这种方式写它,它将打印

$num = 0;
while ( $num < 1 ) {
  $num = $num + 0.1;
  print "$num \n";
}

输出:

0.1 
0.2 
0.3 
0.4 
0.5 
0.6 
0.7 
0.8 
0.9 
1 
1.1 

理想情况下,1和1.1不应分别在两个代码示例中打印。在向其中加入0.1时印刷0.9后,它变为1.0,即while (1.0 < 1)。因此,while循环中的条件为false,因此不应打印1和1.1。但这就是正在发生的事情。

有人可以解释为什么while循环以这种意想不到的方式工作,即即使条件为假也打印1和1.1。

3 个答案:

答案 0 :(得分:4)

1/10是二进制的周期数,就像1/3是十进制周期一样。

          ____
1/10 = 0.00011 base 2

因此,它不能用浮点数精确表示。

$ perl -e'printf "%$.20e\n", 0.1;'
1.00000000000000005551e-01

这种不精确是造成问题的原因。

$ perl -e'my $i = 0; while ($i < 1) { printf "%1\$.3f  %1\$.20e\n", $i; $i += 0.1; }'
0.000  0.00000000000000000000e+00
0.100  1.00000000000000005551e-01
0.200  2.00000000000000011102e-01
0.300  3.00000000000000044409e-01
0.400  4.00000000000000022204e-01
0.500  5.00000000000000000000e-01
0.600  5.99999999999999977796e-01
0.700  6.99999999999999955591e-01
0.800  7.99999999999999933387e-01
0.900  8.99999999999999911182e-01
1.000  9.99999999999999888978e-01

一般来说,可以通过检查数字是否等于某个容差中的另一个来解决这个问题。但在这种情况下,有一个更简单的解决方案。

$ perl -e'for my $j (0..9) { my $i = $j/10; printf "%1\$.3f  %1\$.20e\n", $i; }'
0.000  0.00000000000000000000e+00
0.100  1.00000000000000005551e-01
0.200  2.00000000000000011102e-01
0.300  2.99999999999999988898e-01
0.400  4.00000000000000022204e-01
0.500  5.00000000000000000000e-01
0.600  5.99999999999999977796e-01
0.700  6.99999999999999955591e-01
0.800  8.00000000000000044409e-01
0.900  9.00000000000000022204e-01

上述解决方案不仅执行正确的迭代次数,而且不会累积错误,因此$i总是尽可能正确。

答案 1 :(得分:3)

在第二个版本中,您在循环检查之前打印,即它甚至会打印已经超出循环条件的大小写。

由于浮点精度的原因,你得到1.0以及1.1的事实由1.0略高于添加大量0.1的结果。

所以循环实际看到的是

  • 0.9999&lt; 1.0?是的,打印和循环。打印会进行一些舍入,因此打印的内容为1.0。
  • 1.0999&lt; 1.0?不,但打印不过(因为循环检查在打印后完成。

因此,为了解决问题,请使用第一个版本,但从0.1开始,然后检查0.95。

$num = 0.1;
while ( $num < 0.95 ) {
  print "$num \n";
  $num = $num + 0.1;
}

答案 2 :(得分:2)

考虑一下:

use strict;
use warnings;
use v5.10;

my $num = 0;
while ($num < 1) {
    say $num, " (", $num-1, ")";
    $num += 0.1;
}

输出:

0 (-1)
0.1 (-0.9)
0.2 (-0.8)
0.3 (-0.7)
0.4 (-0.6)
0.5 (-0.5)
0.6 (-0.4)
0.7 (-0.3)
0.8 (-0.2)
0.9 (-0.1)
1 (-1.11022302462516e-16)

正如您所看到的,由于浮点精度问题,重复添加0.1所获得的数字不是一个,而是略小于1,这就是为什么检查比预期成功多一次的原因。

现在将其添加到脚本顶部的导入中:

use bignum;

观察输出变化:

0 (-1)
0.1 (-0.9)
0.2 (-0.8)
0.3 (-0.7)
0.4 (-0.6)
0.5 (-0.5)
0.6 (-0.4)
0.7 (-0.3)
0.8 (-0.2)
0.9 (-0.1)

这是因为计算现在在high-precision mode中完成,并且浮点错误不再生效。

通常,人们会使用浮点算法咬几次,然后学会用整数或epsilons做重要的部分。 Google,有很多关于如何处理浮点精度错误的资源。