代码之谜(五)- 浮点数(谁偷了你的精度?)
博客分类: 代码之谜
代码之谜codepuzzle浮点数 .
****光棍节加长版****
如果我告诉你,中关村配置最高的电子计算机的计算精度还不如一个便利店卖的手持计算器,你一定会反驳我:「今天写博客之前又忘记吃药了吧」。
你可以用最主流的编程语言计算 0.2 + 0.4,如果你使用的是 Chrome、FireFox、IE 8+,可以按 F12 键,然后找到 「控制台」,输入上面的 表达式 0.2 + 0.4,回车。
然后再用最简陋的计算器(如果你没有手持计算器没关系,手机、电脑都自带一个计算器,打开“运行”,输入calc,回车) 再计算一下刚才的 算式 0.2 + 0.4。
怎么样?同意我的观点了吧! 再简陋的计算器也比超级计算器的精度高,关键不在于它的频率和内存,而在于它是如何设计、如何表示、如何计算的。
不能表示 VS 不能精确表示
在上一章『浮点数(从惊讶到思考)』(iteye地址:http://justjavac.iteye.com/blog/1725977)中我们讲到用浮点数表示 数 时出现的问题——很多数都 不能表示。(注意 浮点数表示的是数,而不仅仅是小数。)
如果你数学比较好,或者你确信你身体健康,没有心脏病、高血压,没有受过重大精神创伤,那我告诉你, 在浮点数的表示范围内,有多于 99.999…% 的数在计算机中是 不能表示 的。 真的是太令人吃惊,也太令人遗憾了。 真相总是很残忍。
请注意我使用的措辞,区别开 不能表示 和 不能精确表示。
下面我从数量级分析一下,32bit 浮点数的表示范围是 10 的 38 次方,而表示个数呢,是 10 的 10 次方。 能够被表示的数只有 1/100000000…. (大概有30个零),这个数多大呢?还记得那个国际象棋和麦子的故事吗?
为了让你了解 指数的威力,我再举个例子:
有一张很大很大的纸,对折 38 次,会有多高呢? 一米?一百米?比珠峰还高?再次考验你心脏承受能力的时刻到了:它不仅仅比珠峰高,其实它已经快到达月球了。
回到原来的话题,还有更残忍的真相。 在剩下的可以表示的不到 0.000…1% 的数中,又有多少不能精确表示呢?这就是我写这篇博客的目的。
上一章中我还给出了一种用定点数精确表示小数的方法。 事实上,手持计算器、java 中的 BigDecimal、C# 中的货币类型、MySQL 中的 NUMERIC 类型就是这么干的。 你还记得在数据库中添加字段时的 SQL 语句是如何写的吗?现在明白为什么我说 再简陋的计算器也比超级计算器的精度高 了吧。
这篇博客我将为大家讲解为什么很多数 不能精确表示,本篇可能比较烧脑子,我会尽量用最通俗的语言,最贴近现实的例子来讲解,不在乎篇幅有多长,关键是要给大家讲明白。下一篇,你将了解到浮点数如何工作,以及为什么很多数 不能表示。
热身 —— 问:要把小数装入计算机,总共分几步?你猜对了,3 步。
•第一步:转换成二进制
•第二步:用二进制科学计算法表示
•第三步:表示成 IEEE 754 形式
在上面的第一步和第三步都有可能 丢失精度。
十进制 VS 二进制
下面我们讨论如何把十进制小数转换成二进制小数(什么?你不会?请自觉去面壁)。
考虑我们将 1/7(七分之一) 写成小数的时候是如何做的?
用 1 除以 7,得到的商就是小数部分,剩下的余数我们继续除以 7,一直除到什么时候结束呢? 有两种情况:
1.
如果余数为 0。yeah!终于结束了,洗洗睡吧
2.
当除到某一步时,余数等于 1… 停!stop!等一下,我发现有什么地方怪怪的。余数为 1,余数如果为 1 的话,再继续除下去,不就又是 1/7 了吗?绕了一个大弯,又回来了?对,你猜的很对,它永远不会结束,它循环了。
注意我上面说的 情况2,我们判断他循环,并 不是从直观看感觉它重复了,而是因为在计算过程中,它又回到了开头。为什么这么说呢?当你计算一个分数时,它总是连续出现 5,出现了好多次,例如 0.5555555… 你也无法断定它是无限循环的,比如 一亿分之五。
记得高中时,从一本数学课外书学到了手动开平方的方法,于是很兴奋的去计算 2 的平方根,发现它的前几位是 1.414,哇,原来「2的平方根」等于 1.414141…。很多天以后,当我再次看到我的笔记时,只能苦笑了,「2的平方根」不可能循环啊,它可是一个无理数啊。
你可能不耐烦了,叽哩哇啦说这么多,有用吗?当然有用了,以后如果 MM 问你:你会爱我到什么时候?你可以回答她:我会爱你到 1/7 的尽头。难道我会把我的表白方式告诉你们吗? 我对你的爱就像圆周率,无限——却永不重复。
扯远了,现在会到主题。 你也许会说:我明白了,循环小数不能精确表示,放到计算机中会丢失精度; 那么有限小数可以精确表示吧,比如 0.1。
对于无限小数,不只是计算机不能精确表示,即使你用别的办法(省略号除外),比如纸、黑板、写字板…都无法精确表示。什么?手机?也不能,当然不能了。不,不,iPad也不行,1万买的也不行,真的,再贵的本子也写不下。
哪些数能精确表示?
那么 0.1 在计算机中可以精确表示吗?
答案是出人意料的, 不能。
在此之前,先思考个问题: 在 0.1 到 0.9 的 9 个小数中,有多少可以用二进制精确表示呢?
我们按照乘以 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.2、0.4、0.6、0.8 都不能精确的表示为二进制小数。 难以置信,这可是所有的偶数啊!那奇数呢? 答案就是:
0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确的表示。
如果把 0.0 再算上,那么就有两个数可以精确表示,一个奇数 0.5,一个偶数 0.0。 为什么是两个呢?因为计算机二呗,其实计算机还真够二的。
世界上有 10 种人,一种是懂二进制的,一种是不懂二进制的。
其实答案很显然,我再领大家换个角度思考,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
如果你还想继续思考,就又会发现一个有趣的事实,我们称之为 定理A。 我们上面的数,都是小数点后面一位小数,因此,在十进制中,这样的小数有 10 个(就是 0 到 9); 同理,在二进制中,如果我们让小数点后面有一位小数,应该有多少个呢?当然是 2 个了(0 和 1)。
哇,好像发现了新大陆一样,很兴奋是吧。那我再给你一棒,其实定理A是错的。再重申一遍 尽信书,则不如无书。我写博客的目的 不是把我的思想灌输到你的脑子里,你应该有自己的思想,自己的思考方式,当我得出这个结论时,你应该立刻反驳我:“按照你的思路,如果是 16 进制的话,应该可以精确表示所有的 0.1 到 0.9 的数甚至还可以精确表示其它的 6 个数。而事实呢,16 进制可以精确表示的数 和 2 进制可以精确表示的数是一样的,只能精确表示 0.5。”
那么到底怎么确定一个数能否精确表示呢?还是回到我们熟悉的十进制分数。
1/2、5/9、34/25 哪些可以写成有限小数?把一个分数化到最简(分子分母无公约数),如果分母的因式分解只有 2 和 5,那么就可以写成有限小数,否则就是无限循环小数。为什么是 2 和 5 呢?因为他们是 10 的因子 10 = 2 x 5。
二进制和十六进制呢?他们的因子只有 2,所以十六进制只是二进制的一种简写形式,它的精度和二进制一样。
如果一个十进制数可以用二进制精确表示,那么它的最后一位肯定是 5。
备注:这是个必要条件,而不是充分条件。一位热心网友设计出了下面的解决精度的方案。我就不解释了,同学们自己思考一下吧。
分享到:
相关推荐
《C++编程实例100篇》是一本深入浅出的C++编程教程,它以实践为主导,通过丰富的实例帮助学习者掌握C++语言的基础和核心概念。这本书的每个实例都精心设计,旨在帮助初学者和有经验的开发者巩固和提升C++编程技能。 ...
OSAI系统CNC编程 OSAI系统CNC编程是计算机numerical控制(CNC)系统的一种编程方法,主要应用于计算机辅助制造(CAM)系统中。OSAI编程语言是一种基于文本的高级编程语言,用于编写CNC机床的控制程序。 OSAI系统...
这份考试检测题模拟测试题附答案是为编程猫考试的准备而设计的,测试题涵盖了编程猫的各种知识点,旨在帮助考生更好地了解编程猫的编程语言和逻辑思维能力。 知识点1: 编程猫的造型切换 在这道题中,编程猫的造型...
《Java网络编程第三版》是Java开发者深入理解网络编程的重要参考资料。这本书主要涵盖了Java平台上的网络应用程序开发,从基础概念到高级技术,为读者提供了一套全面的学习路径。以下是本书中涉及的一些关键知识点:...
面向对象编程是计算机语言的一种先进的编程模式,在工业控制系统的PLC程序中也可以采用这种设计思想,虽然我们无法实现面向对象的很多特点如“继承”,甚至于它根本就不具备面向对象编程语言的特点,但面向对象编程...
c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程源代c语言趣味编程...
扫描版 超清百度网盘资源!!! 为初学者准备的网络编程! 韩国TCP/IP经典教程!... 本书针对网络编程初学者,面向具备C语言基础的套接字网络编程学习者,适合所有希望学习Linux和Windows网络编程的人。
新代编程操作手册是针对新代品牌的编程系统提供的一份详尽指南,旨在帮助用户理解和掌握新代编程软件的使用方法。这份手册涵盖了从基本的界面介绍到复杂的编程技巧,旨在提升用户在数控机床编程和控制方面的技能。...
**TOP2005编程器软件** TOP2005编程器是一款专为电子工程师设计的设备,主要用于编程各种微控制器、存储器和其他数字集成电路。它兼容多种芯片,包括常见的8位、16位和32位MCU,以及EPROM、EEPROM、闪存等类型。这...
《UNIX环境高级编程》与《UNIX网络编程卷1:协议》是两本深入解析UNIX系统编程的经典著作。在深入理解这些书籍的知识点之前,我们先要了解UNIX系统的基础概念。 UNIX是一个多用户、多任务的操作系统,其设计哲学...
根据给定文件的信息,我们可以提炼出关于C++游戏编程的一些关键知识点。首先,标题“C++游戏编程入门.pdf”明确指出这是一本介绍如何使用C++进行游戏开发的教材。C++是一种广泛应用于游戏开发的强大编程语言,因为它...
《WINDOWS高级编程指南》是Jeffrey Richter所著,深入探讨了32位Windows平台的高级编程技术。该书是编程领域的经典之作,特别适合于熟悉Windows编程但希望更深入理解其高级概念的读者,如内存管理、消息处理、动态...
【少儿编程】是一种针对儿童和青少年的编程教育,旨在通过早期接触编程,培养他们的逻辑思维、创新能力和问题解决能力。近年来,随着科技的发展和人工智能的崛起,编程技能被视为未来社会的关键素养之一,因此少儿...
CODESYS V3 基础编程指南 CODESYS V3 基础编程指南是 CoDeSys 编程手册的重要组成部分,该指南旨在帮助开发者快速掌握 CoDeSys 编程的基础知识,并应用于实际项目中。以下是 CODESYS V3 基础编程指南中重要的知识点...
Scratch 3.0是麻省理工学院(MIT)媒体实验室终身幼儿园团队开发的一款面向儿童的图形化编程工具,旨在让孩子们通过拖拽积木式的编程块来学习编程概念。"大龙老师新版scratch3.0编程全套素材"提供了一整套的教学资源,...
Python并行编程是指利用Python语言进行多任务同时执行的编程方法。并行编程可以帮助程序在执行多任务时提高效率,尤其是对于计算密集型或I/O密集型的程序。Python支持多线程技术和多进程技术,这些技术分别通过...
编程电缆制作教程是一份珍贵的资源,尤其对于正在使用或准备使用西门子LOGO!系列产品的工程师来说,这份教程无疑是雪中送炭。LOGO!是西门子推出的一种小型可编程逻辑控制器,适用于简单的自动化任务,如开关控制、...
松下PLC(可编程逻辑控制器)编程软件是用于设计、调试和监控松下品牌PLC设备的应用程序。这类软件通常包含多种功能,如编程、模拟、诊断和更新固件,使得工程师能够有效地管理和控制自动化系统。以下是关于松下PLC...
Delta ISPSoft 编程软件的 SFC 编程功能 Delta ISPSoft 编程软件的 SFC 编程功能是一种基于顺序功能图的编程方式,主要用来分析和设计电气操纵系统顺控程序。该编程方式将一个复杂的顺控过程分解为一个个小的工步...
S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程安装包SP9S7-200编程...