浮点数的输入以及浮点数运算

浮点数的输入以及浮点数运算

写在前面

上一次我们讲解了IEEE的标准,还记得多少?

之前我提到过,有很多小数是二进制浮点数无法表示的,因此就难免会遇到舍入的问题.这一点其实在我们平时的计算当中会经常出现,就比如我们之前提到过的0.3,就无法使用浮点小数来准确表示.

 

我使用C#写了一个程序,打印出0.3的二进制表示,是这样的一个数字:0 01111101 00110011001100110011010.不信没关系,用我昨天说的那个公式计算一下啊.这个二进制数大概是多少,它的阶码在偏置之后的值为-2,它的尾数位在加1之后为1+1/8+1/16+1/128+1/256=1.19921875.后面还有有效位,不过我们只是大概的计算一下,不用很精确,结果大概是0.29.(我说这个玩意就是想证明一件事,十进制的小数无法用二进制来准确表示).

 

 

 

如何把带小数的十进制转换成二进制数?

让大哥教你们一招好办法,怎么简单的转换,以及怎么判别能否用给定的位数精确表示一个10进制小数。
0.635为例,这个数能用二进制小数表示么,能的话需要多少位呢。
因为0.635*256=162.56,注意小数部分不为0。所以8位二进制小数无法精确表示。再看0.635*65536=41615.36。小数部分仍不为0,所以16也不能精确表示。
到这里相信大家也看出点什么了,看一个十进制小数能否用给定的n位二进制小数表示,就用那个十进制乘以2n次方,看小数部分是否为零,是的话就能用n位精确表示,否的话就不能。
   其实楼上的52.63可以转换的,8位表示小数部分,把0.63*256也就是28次方得到161.2323不要,把161化成二进制表示为10100001,再连上52的,就成了1010010.10100001,这里用8位表示小数是表示不尽的,因为0.63*256=161.23,小数部分不为零。

 

 

 

 

浮点数的舍入

在我们平时使用的十进制当中,我们一般会对一个无理数或者有位数限制的有理数进行舍入时,大部分时候会采取四舍五入的办法,这算是一种比较符合我们期望的方式.

不过针对浮点数来说,我们的舍入方式会更丰富一些.一共有四种方式,分别是向偶数舍入,向零舍入,向上舍入,向下舍入.

 

这四种方式不难理解,其中向偶数舍入就是向最靠近的偶数舍入,比如将1.5舍入为2,0.1舍入为0.而向零舍入则是向靠近零的值舍入,比如将1.5舍入为1,0.1舍入为0.对于向上摄入来说,则是往大了(也就是正无穷大)摄入的意思,比如将1.5舍入为2,-1.5舍入为-1.而向下舍入则与向上舍入相反,是向较小的值(也就是负无穷大)舍入的意思.

 

这里需要所明一点,除了向偶数舍入除外,,其他三种方式都会有明确的边界.这里的含义是指这三种方式舍入后的值x与舍入之前的值x’会有一个明确的大小关系,比如对于向上舍入来说,则一定有x<=x’.对于向下舍入来说,一定有|x|>=|x’|.

 

对于向偶数舍入来讲,它最大的作用是在统计时使用.向偶数舍入可以让我们在统计时,将舍入产生的误差平均,从而尽可能的抵消.而其他三种方式在这一方面都是有缺陷的,向上和向下舍入很明显会造成指的偏大或者偏小.对于向零舍入来说,如果全是正数的时候会造成结果偏小,全是负数会造成结果偏大.

 

通常情况下我们采取的舍入规则是在原来的值是舍入值的中间值时,采取向偶数舍入,在二进制中,偶数我们认为是末尾为0的数.而倘若不是这种情况的话,则一般会有选择性的使用向上和向下舍入,但总会向最接近的值舍入.其实这正是IEEE才去的默认的舍入方式,因为这种舍入方式总是企图向最近的值舍入.

 

比如对于10.10011这个值来讲,当舍入到个位数时,会采取向上舍入,因此此时的值为11.当舍入到小数点后1位时,会采取向下舍入,因此此时的值为10.1.当舍入到这个小数点后4位时,由于此时的10.10011舍入值的中间值,因此采取想偶数舍入,此时舍入后的值为10.1010.

 

 

 

 

程序当中的浮点数舍入

之前讲解了一堆舍入的方式,最终我们得出一个结论,就是IEEE标准默认的舍入方式,是企图向最近的值舍入.

之前我们已经详细的解释了IEEE标准中默认的舍入方式.但是估计还有人不是很明白,没关系,咱们看看具体的案例.

 

在看案例之前,暂满需要先了解一些中间值的概念.中间值就是指的,比如1.1(二进制)这个数字,假设要舍入到个位,那么他就是一个中间值,因为它处于1(二进制)10(二进制)的中间,在这个时候将会采用向偶数舍入的方式.

public class Main{

    

    public static void main(String[] args){

        System.out.println("舍入前:         10.10011111111111111111101");

        System.out.print("舍入后:");

        printFloatBinaryString(2.62499964237213134765625f);

        System.out.println();

        System.out.println("舍入前:         10.10011111111111111111111");

        System.out.print("舍入后:");

        printFloatBinaryString(2.62499988079071044921875f);

        System.out.println();

        System.out.println("舍入前:         10.10011111111111111111101011");

        System.out.print("舍入后:");

        printFloatBinaryString(2.62499968707561492919921875f);

        System.out.println();

        System.out.println("舍入前:         10.10011111111111111111100011");

        System.out.print("舍入后:");

        printFloatBinaryString(2.62499956786632537841796875f);

        System.out.println();

        System.out.println("舍入前:        -10.10011111111111111111101");

        System.out.print("舍入后:");

        printFloatBinaryString(-2.62499964237213134765625f);

        System.out.println();

        System.out.println("舍入前:        -10.10011111111111111111111");

        System.out.print("舍入后:");

        printFloatBinaryString(-2.62499988079071044921875f);

        System.out.println();

        System.out.println("舍入前:        -10.10011111111111111111101011");

        System.out.print("舍入后:");

        printFloatBinaryString(-2.62499968707561492919921875f);

        System.out.println();

        System.out.println("舍入前:        -10.10011111111111111111100011");

        System.out.print("舍入后:");

        printFloatBinaryString(-2.62499956786632537841796875f);

        System.out.println();

    }

    

    public static void printFloatBinaryString(Float f){

        char[] binaryChars = getBinaryChars(f);

        for (int i = 0; i < binaryChars.length; i++) {

            System.out.print(binaryChars[i]);

            if (i == 0 || i == 8) {

                System.out.print(" ");

            }

        }

        System.out.println();

    }

    

    public static char[] getBinaryChars(Float f){

        char[] result = new char[32];

        char[] binaryChars = Integer.toBinaryString(Float.floatToIntBits(f)).toCharArray();

        if (binaryChars.length < result.length) {

            System.arraycopy(binaryChars, 0, result, result.length - binaryChars.length, binaryChars.length);

            for (int i = 0; i < result.length - binaryChars.length; i++) {

                result[i] = '0';

            }

        }else {

            result = binaryChars;

        }

        return result;

    }

 

}

 

 

代码从别人那里copy,JAVA这么火,你不会不了解吧..反正我是不了解.

 

 

分析一下:上面一共有8次舍入,前四次是整数,后四次是负数.可以看出来对于负数来说,舍入后的位表示是一样的.只是最高位的符号位不同而已,因此这里就不再分析下面四个复数的舍入方式了,主要来看前四次舍入.

 

第一次和第二次对于末尾0111的舍入,由于是中间只,因此全部采取向偶数舍入的方式,保证最低位为0.第三次由于比中间值大,,而数值又是整数,因此采取向上摄入的方式,第四次则比中间值小,数值同样也是整数,因此采取向下舍入的方式.

因为本屌最近在搞C#,使用了C#的代码演示了一遍,采用的是同样的舍入方式.

 

 

 

浮点数运算

IEEE标准中,指定了关于浮点数的运算规则,就是我们将两个浮点数运算后的精确结果的舍入值,作为我们最终的运算结果.正是因为有了这一个特殊点,造成了浮点数运算中,很多运算不满足我们平时熟知的一些运算特性.

比如加法的结合律:a+b+c=a+(b+c),这是很简单的加法特性,但是浮点数不满足这一点,案例如下:

 public static void main(String[] args){        System.out.println(1f + 10000000000f - 10000000000f);        System.out.println(1f + (10000000000f - 10000000000f));    }

这一段代码会依次输出0.01.0,正是因为舍入而造成了这一误差.在第一条输出语句中,计算1f+1000000000f,会将1这个有效数值舍入掉,而导致最终结果为0.0.而在第二个输出语句中10000000000f-10000000000f将先得到结果0.0,因此最终的结果为1.0,因此最终结果为1.0.

(JAVA代码在这里就证明了浮点数运算不满足结合律这一特性,我用了C#代码反而两个输出的结果一样呢?证明不了浮点数不满足结合律呢?我狠狠的给了自己一个大嘴巴子....)

有问题得解决啊,于是乎我开始了下面的扯犊子..

在使用JAVA的时候,

public class Test
{
  public static void main(String[] args)
  {
  double x = 0.01;
  double y = 0.09;
  System.out.println(x + y);
  }
}

为什么输出结果是0.09999999999999999而不是0.1啊?
奇怪的是当xy改为float后,结果就等于0.1了,
更奇怪的是,如果把xy分别改为float0.010.04,在相加,结果居然是0.049999997
这种浮点运算不精确的背后原理到底是什么呢?

 

可是我在使用C#的时候:

class Test

    {

        static void Main()

        {

             double x = 0.01;

             double y = 0.09;

             Console.WriteLine(x + y);

             Console.ReadKey();

        }

    }

结果是0.1,为啥呢?

第一点,你要明确java里面32位的float0.1在内存当中的表示是不精确的,不是你理解的那么100%精确,抛弃这个概念,产生这个误差的原因简单理解在32位的float的表达能力有限和计算机的二进制缘故吧,往下说也很复杂。
大体说下,float0.1二进制形式是001111011 10011001100110011001101,根据符号位换算为10进制表达的值精确应该是这样计算 110011001100110011001101乘以2的负27次方,实际值是0.100000001490116119384765625
这样就产生了实际误差
这个误差对我们生活小打小闹没啥影响,但是对科学计算和银行这样的应用或者领域是致命的,因此要用Java银行以及科学计算会用java.math.BigDecimal提高精度,否则后果极其严重。

看一段C#代码:

            float Fvar = 1f;            double Dvar = (double)Fvar;            Console.WriteLine("Float Var={0};Double Var={1}", Fvar, Dvar);             float Flt = 0.9f;            double Dbl = (double)Flt;            Console.WriteLine("Float Var={0};Double Var={1}", Flt, Dbl);

即在C#中,当float转换为double时,若float数值不带小数,可得出正确值;但包含小数时,会得到一个近似的“超级”小数。

1、这是由于小数精度引起的,小数转换二进制是乘2取整的,但是对于那些永远也乘不完的数,比如你这个0.9,那么它会一直取下去知道位数满了。所以肯定是不准确的。
2floatdouble需要补位,这样就有误差了。举个例子,float的精度可能为0.001,而double的精度可能为0.00001这样转换后可能会有0.001 - 0.00001的误差。
所以应该使用decimal

看代码:

 float Fvar = 1f;             double Dvar = (double)Fvar;             Console.WriteLine("Float Var={0};Double Var={1}", Fvar, Dvar);             float Flt = 0.9f;             double Dbl = (double)Flt;             decimal Dec = (decimal)Flt;             Console.WriteLine("Float Var={0};Double Var={1};Decimal Var={2}", Flt, Dbl,Dec);             Console.ReadLine();

 

其实相应的,浮点数的乘法也不满足结合律,就是说:a*b*c!=a*(b*c),同时也不满足分配律,a*(b+c)!=a*b+a*c.浮点数失去了运算数的很多方面的特性,因此也导致很多优化手段无法进行,比如我们试图优化这样一段代码(JAVA为例,因为JAVA的实现具有代表性).

/*   优化前       */

        float x = a + b + c;

        float y = b + c + d;

        /*   优化后       */

        float t = b + c;

        float x = a + t;

        float y = t + d;

 

对于优化前的代码来讲,进行了4次浮点运算,而优化后则是3次。然而这种优化是编译器无法进行的,因为可能会引入误差,比如就像前面的小例子中的结果01一样。编译器在此时一般是不敢进行优化的,试想一下,如果是银行系统的汇款或者收款等功能,如果编译器进行优化的话,很可能一不小心就把别人的钱给优化掉了。

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值