A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 黄玉昆 黑马帝   /  2013-3-5 09:18  /  2536 人查看  /  8 人回复  /   2 人收藏 转载请遵从CC协议 禁止商业使用本文

今天看到了一篇很有意思的文章,是关于浮点数精度问题的。讲的深入浅出,幽默迭起,忍俊不禁。我文笔拙劣,在这里就把认为比较精华的部分,总结了一下,分享给大家,希望能对大家有帮助。
计算机的精度够不够高?你一定会信心满满地说:这是木庸置疑的。但如果我说,就算是硅谷最高配置的电子计算机的精度还不如你家的计算器精度高,你一定会说我今天忘带大脑出来了(本人宅男,大脑一直在身上,嘿嘿)。
不信?!好吧,那你可以测试一下:用0.2和0.4相加,看看结果,输出0.6,运算结果是错误的,你一定觉得,我在忽悠你,注意:我说的是运算结果,不是显示结果,你可以再自己写一个程序,用循环算出十个0.1的和是否是1.0。
你再用计算器算一遍,十个0.1相加是不是1.0.怎么样,这次信了吧!即使你掰掰手指头都要比超级计算机准确,不过别对计算机失去信心啊,这关键不在于它的频率和内存,而在于它的计算方式,表示方法,以及设计形式。
不能表示 VS 不能精确表示:
如果你数学比较好,或者你确信你身体健康,没有心脏病、高血压,没有受过重大精神创伤,那我告诉你,在浮点数的表示范围内,有多于 99.999…% 的数在计算机中是 不能表示 的。 真的是太令人吃惊,也太令人遗憾了。真相总是很残忍。
请注意我使用的措辞,区别开不能表示 不能精确表示。
下面我从数量级分析一下,32bit浮点数的表示范围是 10 38 次方,而表示个数呢,是 10 10 次方。 能够被表示的数只有 1/100000000…. (大概有30个零),这个数多大呢?还记得那个国际象棋和麦子的故事吗?回到原来的话题,还有更残忍的真相。 在剩下的可以表示的不到 0.000…1% 的数中,又有多少不能精确表示呢?这就是这篇文章的目的。
热身一问:要把小数装入计算机,总共分几步?你猜对了,3 步。
第一步:转换成二进制
第二步:用二进制科学计算法表示
第三步:表示成 IEEE754 形式
在上面的第一步和第三步都有可能 丢失精度。
十进制 VS 二进制
下面我们讨论如何把十进制小数转换成二进制小数(什么?你不会?那就百度去吧,好吧,还是我给你说说吧)。
如果你看了毕老师的视频,你一定会计算6的二进制形式,那么我想说,你帮我算算0.6的二进制吧,并请写出来,我将万分感激你了,而且你绝对是人才。我敢这么说,那就说明,你一定写不出来。不信?!好吧,我不忽悠你了,接着看:
你可以去网上百度一下,你就知道了,想用全世界的张纸写下0.6的二进制数,那是天方夜谭,因为它在循环。对于循环数,不只是计算机不能精确表示,即使你用别的办法(省略号除外),比如纸、黑板、写字板都无法精确表示。什么?手机?也不能,当然不能了。不,不,iPad也不行,1万买的也不行,真的,再贵的本子也写不下。
十进制转为二进制的算法:
十进制数转化为二进制数时,整数部分和小数部分要用不同的方法来处理。整数部分的转化采用除基取余法:将整数除以2,所得余数即为2进制数的个位上数码,再将商除以2,余数为八进制十位上的数码……如此反复进行,直到商是0为止;对于小数的转化,采用乘基取整法:将小数乘以2,所得积的整数部分即为二进制数十分位上的数码,再将此积的小数部分乘以2,所得积的整数部分为二进制数百分位上的数码,如此反复……直到积是0为止。
言归正传:那么 0.1 在计算机中可以精确表示吗?答案是出人意料的,不能。我们按照乘以 2 取整数位的方法,把 0.1 表示为二进制(我假设那些不会进制转换的同学已经补习完了):
(1) 0.1 x 2 = 0.2  取整数位 0 0.0
(2) 0.2 x 2 = 0.4  取整数位 0 0.00
(3) 0.4 x 2 = 0.8  取整数位 0 0.000
(4) 0.8 x 2 = 1.6  取整数位 1 0.0001
(5) 0.6 x 2 = 0.2  取整数位 1 0.00011
(6) 0.2 x 2 = 0.4  取整数位 0 0.000110
(7) 0.4 x 2 = 0.8  取整数位 0 0.0001100
(8) 0.8 x 2 = 1.6  取整数位 1 0.00011001
(9) 0.6 x 2 = 1.2  取整数位 1 0.000110011
(n) ...
我们得到一个无限循环的二进制小数 0.000110011…
我为什么要把这个计算过程这么详细的写出来呢?就是为了让你看,多看几遍,再多看几遍,继续看还没看出来,好吧,把眼睛揉一下,我提示你,把第一行去掉,从 (2) 开始看,看到 (6),对比一下 (2) (6)然后把前两行去掉,从 (3) 开始看明白了吧,0.20.40.60.8 都不能精确的表示为二进制小数。 难以置信,这可是所有的偶数啊!那奇数呢?答案就是:0.1 0.9 9 个小数中,只有 0.5 可以用二进制精确的表示。如果把 0.0 再算上,那么就有两个数可以精确表示,一个奇数 0.5,一个偶数 0.0 为什么是两个呢?因为计算机二呗,其实计算机还真够二的。其实答案很显然,我再领大家换个角度思考,0.5 就是一半的意思。 在十进制中,进制的基数是 10,而 5 正好是 10 的一半。 2 的一半是多少?当然是 1 了。 所以,十进制的 0.5 就是二进制的 0.1。如果我用八进制呢? 不用计算你就应该立刻回答:0.4;转换成十六进制呢,当然就是0.8 了。
(0.5)10 = (0.1)2 = (0.4)8 =(0.8)16


8 个回复

倒序浏览
那么到底怎么确定一个数能否精确表示呢?还是回到我们熟悉的十进制分数。
如果一个十进制数可以用二进制精确表示,那么它的最后一位肯定是 5。备注:这是个必要条件,而不是充分条件。作者观点,针对小数精度不够的问题(例如 0.1),软件可以人为的在数据最后一位补 5, 也就是 0.15,这样牺牲一位,但是可以保证数据精度,还原再把那个尾巴 5 去掉。
请同学们思考一下。精度在哪儿丢失?
在 java 中计算 0.2 + 0.4 得到的结果是
// 代码(a)
double d = 0.2 + 0.4;  // 结果是 0.6000000000000001
但是当直接输出 0.6 的时候,确实是 0.6
// 代码(b)
double d = 0.6;  // 结果是 0.6
好像很矛盾。很显然,通过代码(b)可以知道,在 java 中,可以精确 显示 0.6,哪怕 0.6 不能被精确表示,但至少能精确把 0.6 显示出来,这不是和代码(a)矛盾了吗?这又是一个 想当然的错误,在直观上认为 0.2 + 0.4 = 0.6 是必然成立的(在数学上确实如此),既然(a)的结果是 0.6,而且 java 可以精确输出 0.6,那么代码(a)的结果应该输出 0.6。其实在计算机上 0.2 + 0.4 根本就不等于 0.6,因为 0.2 和 0.4 都不能被精确表示。 浮点数的精度丢失在每一个表达式,而不仅仅是表达式的求值结果。
我们用数学中的概念类比一下,比如四舍五入,我们计算 1.6 + 2.8 保留整数。
1.6 + 2.8 = 4.4
四舍五入得到 4。我们用另一种方法:先把 1.6 四舍五入为 2;再把 2.8 四舍五入为 3;最后求和 2 + 3 = 5
通过两种运算,我们得到了两个结果 4 和 5。同理,在我们的浮点数运算中,参与运算的两个数 0.2 和 0.4 精度已经丢失了,所以他们求和的结果已经不是 0.6 了。
回复 使用道具 举报
{:soso_e179:}
回复 使用道具 举报
赞个   
回复 使用道具 举报
谁偷了我的版主名额,最后发现原来是你。{:soso_e104:}
回复 使用道具 举报
收藏了。。。。
回复 使用道具 举报
滔哥 发表于 2013-3-5 11:21
谁偷了我的版主名额,最后发现原来是你。

:lol      
回复 使用道具 举报
收藏啦~
回复 使用道具 举报
原来我们学的最基础的Java基础中的浮点数都可以被你说的如此高深,必须的佩服啊,努力中!
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马