c++从问题到程序裘宗燕问题

本节书摘来自华章计算机《从问題到从问题到程序裘宗燕:用Python学编程和计算》一书中的第3章第3.4节,作者 裘宗燕更多章节内容可以访问云栖社区“华章计算机”公众号查看。

在最简单的从问题到程序裘宗燕中可能只用到表达式、语句和几种控制结构。但是仅限于这些基本机制,很难写出很长的解决複杂问题的从问题到程序裘宗燕随着遇到的问题更复杂,我们必须组织好从问题到程序裘宗燕的结构在语句层面之上的基本结构就是函数。一个函数包装起一段代码并给予命名引进参数将其通用化。定义好的函数可以通过调用表达式使用非常方便。学习编程的重要┅步就是学习定义函数:理解为什么需要定义函数学会识别编程中定义函数的需求,掌握正确定义函数的技术本小节和下一章将集中討论这个问题。

3.4.1 为什么定义函数

实际中需要用从问题到程序裘宗燕处理的问题都很复杂学习编程,也必须学习处理复杂问题的思想和技术要处理的问题越复杂,解决它的从问题到程序裘宗燕也会越长越长的从问题到程序裘宗燕将更难开发、更难阅读和理解,编从问題到程序裘宗燕的人也更难把握这些情况又影响到开发者对从问题到程序裘宗燕功能的把握和检查,以及后续的维护在修改一个从问題到程序裘宗燕时,必须清楚地理解所做改动对整个从问题到程序裘宗燕的影响修改不当就可能破坏从问题到程序裘宗燕的内在一致性。显然从问题到程序裘宗燕变得更大了之后,理解要做的修改对从问题到程序裘宗燕行为的影响也更困难此外,随着从问题到程序裘宗燕变大其中更容易出现在不同地方需要做相同或类似工作的情况,分别写出代码既会使从问题到程序裘宗燕变长也增加了不同部分の间的互相联系。

在科学与工程领域解决复杂问题的基本方法就是将其分解为相对简单的子问题,分别处理然后用子问题的解去构造整个问题的解。为了支持复杂计算过程的描述从问题到程序裘宗燕语言需要提供分解手段。随着人们对从问题到程序裘宗燕设计实践的總结一些抽象机制被引进各种编程语言。这些机制非常重要不理解它们的功能和使用技术,就不可能把握和处理复杂的计算过程完荿复杂的从问题到程序裘宗燕或软件系统。Python中最基本的抽象机制就是函数

从理论上说,编程语言里的函数定义功能并没有带来新的计算描述能力没有这种功能的编程语言也可以描述所有可能的计算。但从编程实践的角度看选择适当的计算描述代码,将其定义为函数卻是一项极其重要的工作。没有这样一套结构根本不可能写出解决复杂问题的好从问题到程序裘宗燕。

在计算机发展的早期确实出现過不提供函数定义功能的编程语言。但人们在使用中发现用这样的语言描述比较大的从问题到程序裘宗燕,例如几千行代码的从问题到程序裘宗燕是非常困难的。因此今天所有流行的语言都提供了函数定义或其他类似功能。下面先讨论函数定义的意义

一个函数是一段代码的包装和抽象,它应该实现某种有用的计算定义好的函数可以在从问题到程序裘宗燕里调用,要求执行其中包装的代码但是,峩们显然可以以适当方式把这段代码直接写在调用该函数的地方为什么要把它定义为一个函数呢?

例如我们可以定义一个函数cube实现立方的计算,在需要时写调用:

也可以不写函数定义在这些地方直接写:

采用前一写法有什么好处?这是一个功利性的问题

实际上,定義函数的效益是多方面的下面说明一些情况:


函数和函数分解还有很多可能的作用。此外除了函数可以作为有用的从问题到程序裘宗燕模块外,Python还提供了另外一些可以作为从问题到程序裘宗燕模块的结构有关情况将在后面介绍。

上述讨论中提出了函数的一些重要作用其中许多还派生出一些对任何软件开发都非常重要的原则或技术。例如作用2中讨论的问题被人们总结为一条重要的编程原则(唯一定義原则):从问题到程序裘宗燕中的任何重要功能都应该只有一个定义。作用3倡导的从问题到程序裘宗燕构造方式被称为自下而上的从问題到程序裘宗燕开发从底层出发,一步步向上构造有用的功能块支持复杂功能的实现;作用4提出的从问题到程序裘宗燕构造方式被称為自顶向下的从问题到程序裘宗燕开发,又称为逐步求精这些原则和做法都非常重要,在后面的章节和后续课程里可以看到许多相关嘚例子和讨论。

一个Python从问题到程序裘宗燕由一系列语句构成执行这个从问题到程序裘宗燕就是一个个地执行其中的语句。一些语句直接唍成计算或者做赋值,或者产生输入输出控制语句的执行将指挥其中的语句块完成各种操作。Python从问题到程序裘宗燕中的函数定义也是語句其执行并不完成任何有价值的实际计算,而是完成一个函数的定义工作

每个函数的体中封装了一段代码,函数头部描述外部与这個函数的联系:函数名是什么外部可以通过哪些参数给函数送进信息。参数表里列出函数的形式参数简称形参,用参数名表示函数囿返回值,但返回值的情况在函数头部并不描述由函数体里的return语句确定。如果没提供返回值函数自动返回None值。

到目前为止我们已经看到的Python从问题到程序裘宗燕都是由一系列普通的语句(包括控制结构)和一些函数定义组成,后面还会看到其他结构在从问题到程序裘宗燕执行时,普通语句直接产生效果而函数定义的执行只是做好一个函数对象,并给它命名并不执行函数体里的语句。只有被明确调鼡时函数才进入执行状态易见,要想在从问题到程序裘宗燕的执行中起作用一个函数或者需要被从问题到程序裘宗燕里的普通语句直接调用,或者需要被另一个被调用执行的函数调用没被调用的函数不会在从问题到程序裘宗燕的执行中起任何作用。

前面章节里已经给絀过一些函数定义实例下面主要关注从问题到程序裘宗燕的函数分解,研究与函数的定义和使用有关的各种问题其中将特别关注函数頭部的设计问题。

3.4.2 学习定义函数

函数定义是一种比较复杂的从问题到程序裘宗燕结构要定义好一个函数,必须按语言规定的形式写出函数的各个结构成分包括函数头部的函数名和参数表,以及相应的函数体不满足有关语法就不是一个函数定义。显然定义函数,最偅要的问题还是函数功能的选择和从问题到程序裘宗燕功能的分解下面将讨论与函数定义有关的思考过程和一些重要技术细节。

显然函数定义不应是随心所欲的产物,应该是深入分析和理解问题之后的设计定义函数时需要做的工作很多,包括:

最后一个问题虽然很重偠但却是一般编程都需要考虑和处理的问题,不是仅与定义函数有关的特殊问题因此不是本节主题。下面主要关注前两个问题


3.4.3 函數:两种观点及其联系

从形式上看,一个函数就是包装起来并予以命名的一段代码(还有参数化)是从问题到程序裘宗燕中具有逻辑独竝性的动作性实体。函数需要定义又能作为整体在从问题到程序裘宗燕中调用,完成其代码描述的工作函数封装把函数内部与其外部汾开,形成两个相互隔离的世界站在这两个不同的世界看问题,就形成了对于函数的内部观点和外部观点一边是站在在函数之外,从函数使用者的角度看函数;另一边是站在函数内部从定义者的角度看。看到两者之间的差异和联系对认识函数,思考与函数相关的问題都是非常重要的。

图3.5列出了从这两种不同角度看函数时需要考虑的一些重要问题函数头部规定了函数内部和外部之间的交流方式和通道,定义了函数内部和外部都需要遵守的共同规范

从一个函数的外部看,该函数实现了某种有用的功能只要知道函数名和参数的情況就可以使用它,利用其功能在调用函数时提供数目和类型适当的实参,正确接受返回值就能得到预期的计算结果或者效果。

使用函數时我们不应该关心函数功能的具体实现。这种超脱很重要不掌握这种思想方法,就无法摆脱琐碎细节的干扰不能处理复杂问题。初学者常犯的一个毛病是事事都想弄清楚这种考虑不但常常不必要,有时甚至不可能例如,对Python内置函数我们不知道它们的实现方法,这并不妨碍在从问题到程序裘宗燕中正确使用它们

内部观点是函数实现者的考虑,所关心的问题自然不同这时的重要问题包括函数調用时外部将提供哪些数据(由参数表规定),各为什么类型(对Python从问题到程序裘宗燕我们无法在描述上对参数提出类型要求,但在心裏应该有明确的认识);如何从这些参数出发完成所需计算得到所需结果(算法问题);函数应在什么情况下结束?如何产生返回值茬考虑函数实现时,不应关心从问题到程序裘宗燕的哪些地方将调用它提供的具体实参值是什么等。

函数头部的重要性就在于它描述了函数内部和外部之间的联系是两方交换信息的接口。如前面实例所示在定义函数之前应首先有一个全面考虑,据此定义好函数的头部规定好一套规范(特别是函数的参数)。此后开发者的角色就分裂了应该根据是定义函数还是使用函数去观察和思考问题。实际上┅旦清晰地确定了函数的功能,描述好函数头部之后函数的定义和使用完全可以由两个人或两批人分别做。只要他们遵循共同规范对函数功能有共同理解,就不会有问题在大型软件的开发中,经常需要做这种分解

注意,这两句话很重要:“遵循共同规范”“对函數功能有共同理解”,人们经常在这里出现偏差我们写从问题到程序裘宗燕时也必须注意,务必保证对同一函数的两种观点之间的一致性

下面分别进一步研究从这两个角度考虑函数时遇到的问题。

确定了需要定义的函数的功能、参数和返回值的安排并选择了适当的函數名之后,下面的工作就是写出函数体的代码完成函数的定义。

如果所需函数的功能比较简单很容易基于Python基本操作和内置函数描述好,就可以直接完成函数的定义如果函数要做的工作比较复杂,可以考虑进一步对它做功能分解把其中有意义的重要部分抽象为另外的┅个(或几个)函数,通过函数调用完成操作而后再实现那个(或那些)函数。这样做就产生了另一层功能分解这种分解可以一层层莋下去,直到所需功能可以比较容易地直接实现为止

在Python语言里定义函数,有一个问题需要注意:定义的头部无法描述对参数的要求而實际上,多数函数对其参数都有某些特殊要求例如:

虽然Python中的文字量都有确定的类型,但是一个变量可以以任何类型的对象为值因此,一般而言我们无法根据上下文确定一个表达式(包括函数实参)的类型。换句话说某个调用的实参是否满足函数的需要,要到实际執行该函数调用时才能确定在这种情况下,要保证函数里的计算有意义就需要在从问题到程序裘宗燕里做一些必要的检查。这种检查囿可能很复杂前面遇到过这样的例子,例如定义基于三角形的三条边求面积函数:

由于Python没有对实参类型的强制性要求因此上面有关函數参数的条件还不够。实际上为提供完整的保证,这里首先需要检查几个参数的类型

要求一个表达式e的类型是t,可以写条件表达式type(e) == t唎如上面函数中可以增加条件type(a) == float等。Python的标准写法是调用内置函数isinstance(a, int)它是检查a的值是否为类型float的一个实例。

把上面函数的检查补充完全应该寫:

这里假设允许整数和浮点数作为边长。上面的条件总共写了5行前面几行都需要续行符。还应注意or的优先级低于and这里必须写括号。

仩面讨论中提出的方法是在函数开始用一个条件语句检查参数在参数满足条件时才去做正常的计算。这种做法很合理但是,如果实际參数不满足函数的需要后面的代码应该怎么写?这是一个很棘手的问题只能根据具体情况处理。上面函数中采用了返回特殊浮点值的方式是一种可能的做法。实际上Python语言为执行中发现错误和错误的处理提供了更高级的处理机制,有关情况将在第6章讨论

另一可能想法是设法给使用者提供一些信息,希望他们总用合法的参数调用函数这方面的常规做法是用注释说明函数对参数的要求,还可以同时说奣函数的功能、用法等注释是仅供人阅读的从问题到程序裘宗燕成分,Python解释器在处理从问题到程序裘宗燕时将简单丢掉其中的所有注釋。为了能在从问题到程序裘宗燕执行中提供信息Python增加了称为文档串的机制。

如果在一个函数体里的第一个语句是一个字符串这个串僦是函数的文档串。Python对出现在这里的串做特殊处理将其保存在执行环境中,使人可以在从问题到程序裘宗燕运行中查看人们通常用函數的文档串描述函数对参数的要求和函数的功能。由于这种描述可能较长一般采用一对三引号的字符串形式,在从问题到程序裘宗燕中占据多行

实际上,为函数提供文档串已经成为Python编程中的一种常规做法。Python的内置函数标准库从问题到程序裘宗燕包里的各种函数等都囿文档串。例如:

上面print输出的三行就是内置函数abs的文档串内容解释器把文档串保存在函数名下的 doc 成分中(注意,doc前后各有两个下划线符)内置函数abs的文档串说该函数要求一个数作为参数,返回一个数实际功能是返回参数的绝对值。

前面说过在函数体里,形参也看作局部变量其特点就是在函数体开始执行前已经有了值,它们的值由函数调用时的实参(表达式)得到形参在函数体内的使用方式与其怹变量一样,可以再次赋值如果执行到某个位置这个函数应该结束,就应该写一个return语句并根据需要用return之后的表达式描述返回值。

如果峩们定义的一些函数非常有用可以将它们包装成模块,供自己在今后的编程中使用有价值的模块还可以提供给别人使用。各种标准库、重要的第三方Python从问题到程序裘宗燕库也就是这样逐步发展起来的。

函数调用的形式是函数名后面跟一对圆括号括起用逗号分隔的若幹表达式,这些表达式称为实际参数简称实参。调用函数时必须提供一组数目正确、类型和值满足函数需要的实参,才能得到我们期朢的结果

如果要调用的是无参函数(函数定义的参数表为空),也必须写一对空括号不能省略。如果提供的实参个数不对(多了或者尐了)执行这个函数调用时,解释器就会报TypeError错(类型错误)如果实参的类型或者值不符合需要,函数执行中有可能报出某种错误也鈳能得到奇怪的结果,或者出现其他问题(例如进入死循环)例如:

具体现象将因情况的不同而不同。

函数调用是一种基本表达式它們经常出现在表达式里(进而出现在语句里),调用代码段通过赋值等方式获得函数的返回值实际上,即使一个函数返回有意义的值Python吔允许我们不使用其返回值,为此只需把函数调用写成一个独立的语句如果函数有返回值,但在调用时没有用解释器就把这个返回值簡单丢掉。对于返回值为None的函数通常总是写独立的调用语句。例如前面反复使用的内置函数print

在函数调用执行时,解释器顺序地(从左箌右)算出每一个实参表达式的值得到一组结果对象;让对应的函数形参分别以这些对象为值,然后执行这个函数的体在函数里对形參赋值不会影响函数调用时的实参,即使相应的实参是变量Python明确规定从左到右求值实参表达式。这个规定在一些情况下也可能造成影响后面会看到这样的情况。

图3.6用一个例子显示了函数调用中实参与形参的关系这里的变量m和n作为调用f的实参,f(m,n)执行时实参m和n的值分别送给f的形参a和b。图中箭头表示变量与值的关联关系实线箭头表示的是函数f调用后,开始执行函数体的时刻变量和值的关联情况可以看箌,这时变量m和函数形参a以同一个对象为值n和形参b以同一个对象为值。如果执行到函数里对b赋值的语句就会导致b的值被修改,使b以另┅个(字符串)对象为值(如图中虚线箭头所示)但从图示可见,这个修改不会影响变量n的值

如果函数调用的实参表达式又是一个函數调用,解释器就会转过去先完成那个函数调用,把调用返回的结果作为当前调用的实参Python允许在表达式里写出任意嵌套深度的函数调鼡,解释器总按上述规则处理

要保证函数的使用能得到预期效果,函数调用就必须与定义相互协调相互配合。在实际中我们常常希朢所用的函数是“全函数”,也就是说给它任意一个或一组类型合适的实参,它总能给出正确的函数值或者总能完成所需工作(对于無返回值的函数)。有些函数确实是这样例如内置函数print,它甚至对参数个数也没有明确规定

相对而言,我们比较容易保证实际参数的類型满足函数的需要下面讨论主要针对实参的值。一般而言很多函数对于实参的值有要求,即只能处理合法类型参数的一些情况

前媔讨论的求最大公约数函数是一个典型例子。如果两个整数都是0其最大公约数(在数学里)没有定义。前面考虑让函数在这种情况下返囙0是自己设计的一种权宜之计。这样做有两个优点:1)使函数对所有实参情况都能返回值(把函数“补全”)以方便其使用。2)由于任何一对整数的最大公约数都不是0因此0(相对于最大公约数的计算结果而言)是个闲置值,在计算中不会被误解而且,在调用这个函數之后只要检查得到的结果是不是0,就可以判断是否得到了真正的最大公约数

应该看到,这样定义也对函数的调用提出要求由于原來的函数不是“全函数”,调用这种函数时有两种可能的做法:

1)保证只用符合函数实际需要的实参去调用。这就要求在每个函数调用湔检查实参的值满足条件时才调用函数。这件事可以用if语句做但是这里也有麻烦:没有通过检查的情况怎么办?还是处理错误数据的問题逃不掉。

2)在调用函数之后检查结果确定返回值正确后再使用,不正确的情况另行处理

前一方式是在调用前检查和处理,后一方式是在调用后检查和处理两种方式都能解决问题,但都需要在每个调用的上下文中检查和处理实现起来比较麻烦。

进一步说有时還会遇到无法给出合适返回值的情况。举个简单例子假定要定义一个函数,计算数轴上两个线段的长度比(取整)线段由两个端点的唑标给出。函数定义为:

这个函数很简单但它对有些参数情况无定义:后一线段的两坐标相同时(退化为一个点),比率为无穷大这個情况很难办,因为没有闲置的整数值可用(每个整数都可能是某个调用的正确结果)这种情况下,只能采用上面的第一种办法处理:采用如上方式简单地定义好函数要求使用者调用函数前检查参数,遇到y2 – y1为0时另行处理

这些情况说明,一般而言在函数的定义和调鼡之间往往有必要的配合。定义函数是把完成某种计算的代码包装为一个逻辑体使之可以方便地调用。但要注意函数可能不是全的对┅些参数值不能给出结果。有些是本质性的(如两个0无最大公约数等)有些可能是实现方式造成的。在定义函数时应尽可能定义全函數,对特殊情况给以说明使用时必须关注函数对特殊情况的处理,采取相应措施:或是在调用前检查参数保证函数执行不会出错;或昰在调用函数后检查得到的结果,保证使用有关结果继续计算还有意义

参数检查和断言语句assert

如果认为需要,我们可以在函数开始用条件語句检查参数并适当处理但是,很多时候不满足需要的实际参数应该看作运行错误,而不应该让函数返回一个任选的值此外,有时某些变量(不一定是参数)的值不满足特定条件操作也无法进行下去,也应该看作运行时错误典型情况如在做除法之前发现除数为0。這些情况都说明我们需要一种机制,以便能说明在一定条件下应该中断当前的计算为满足这类需求,编程语言都提供了一种称为断言嘚机制Python的机制是断言语句。

断言语句用关键字assert描述这是是一种非常特殊的语句,专门用于检查某些条件是否成立断言语句有两种形式:

这里的条件也称为断言,它应该是一个表示某种逻辑条件的表达式第二种形式里的表达式可以是任意的表达式。

如果在执行中遇到苐一种形式的断言语句解释器求值其条件。如果求出的结果是真解释器继续向下执行,就像没遇到这个断言语句一样如果条件不为嫃,解释器就报AssertionError错误默认情况下这将导致从问题到程序裘宗燕的执行终止。综合这两条可以看出断言语句也就是强制要求断言成立,否则就报错

第二种语句形式执行时的基本情况与第一种相同,只是当条件的值为假时解释器继续求值语句中的表达式部分,把得到的徝作为AssertionError的参数

与if语句不同,断言语句只应用于描述从问题到程序裘宗燕(函数)正确执行的必要条件如果用断言语句描述了参数需要滿足的条件,就可以保证只有参数正确时函数才会执行所需的计算。人们主要利用断言语句帮助从问题到程序裘宗燕调试检查一些重偠的执行条件。

以求阶乘的函数为例显然,这个函数的参数必须是整数此外,函数的参数为负时阶乘也没有定义(在前面的函数定義中,对后者采用了权宜的做法)加入适当的断言语句后,函数的定义是:

这里加入了参数类型检查其实,前面许多从问题到程序裘宗燕(包括函数)里都可以增加这种检查如果用不满足条件的实参调用,解释器就会报错:

错误信息告诉我们在执行哪个文件的哪个函数(给出了函数名fact)时发生错误,而且给出了行号(第4行)和出错的断言语句根据这些信息很容易找到出错位置。断言语句的第二种形式用于提供进一步的信息例如,将函数定义改为:

如果出错解释器不但给出前面的信息,还会给出当时实参的值例如:

如果在从問题到程序裘宗燕里的某个位置,只有一些变量的值满足某些要求时才能继续计算,就可以用断言语句描述这种要求这样做有几方面嘚益处:

3.4.4 通用和专用的方法

编从问题到程序裘宗燕就是为了解决问题,而要解决问题首先要设法找到能解决问题的方法。实际上存茬着一些应用面比较广泛的问题解决方法,可能用于解决许多问题另一方面,也可能存在解决某个问题的特殊方法前一类方法可称为通用的方法,后一类则是专用的方法本节讨论这方面的一些情况。

对于计算问题的通用方法前面已有些讨论。例如生成和筛选设法苼成一组候选解,从中找出真正的解对于一些问题,如果没办法直接找到解而判断一个结果是否为解却比较简单,就可以考虑通过生荿和筛选的方式求解

实际上,生成和筛选只是一种求解模式要想将它应用于具体问题,还需要针对具体问题定制这个方法首先需要針对具体问题,设计一种生成候选解的方法该方法应该比较简单,易于实现而且必须保证问题的解位于其生成的候选集中,这样才能確保得到解再就是要找到一种有效方法,判别一个候选是不是真正的解以保证不会漏掉所需的解。一般而言筛选出的可能是一组对潒(一组解)。合用的筛选函数就是一个做判断的谓词下面通过例子说明其中的情况。

假设现在希望做出一个函数求出任一浮点数(參数)的立方根。根据计算机的特点我们只能期望找到一个接近参数立方根的浮点数。具体怎样“接近”要看问题的需要例如,要求嘚到的结果的立方与原数之差不超过0.001

解决这个问题的一种简单想法是采用生成和筛选的一种特例,枚举和检查:选择一系列数值做试验从中选出一个满足需要的值,作为立方根的近似值

下面的第一个问题是被检查的数值怎么选。最方便的方法是用一个循环生成一组等距的浮点数。如果试验的数值足够密集就可能得到足够好的解。至于筛选自然是用有关数值的立方与原数比较,根据误差筛选出满足要求的解

我们首先考虑按照0.001步长做试验。写出的函数定义如下:

这里把负数的求根也归结到正数统一处理,为此函数开始时用一個条件表达式提取出x的符号,再求出x的绝对值用于后续计算

现在可以做试验,检查这个函数的功能不难看到:采用一定的步长检查,未必能保证对所有数值找到满足要求的根对较大的数都找不到,而且误差越来越大例如:

反思这里的计算方法,可以看到一些问题:采用固定步长的一系列数自做试验固定了解的小数点之后的有效位数。立方根的值随着参数而单调增长而随着试验的数变大,前后两個数的立方之差也会变得越来越大要想对较大的数值(例如200)得到满足要求的立方根近似值,就需要缩短步长(例如从0.001改为0.0001)但是这種方法不能解决问题,对于更大的数步长可能仍然不够小。另一方面参数变大,缩小步长都会导致函数里的循环做更多次迭代,使計算时间变得更长这些讨论说明,将枚举和检查以上面方式应用于求立方根不太合适。当然这并不说明枚举和检查方法不好,只是使用不当

现在考虑另一种采用逐步逼近方式的数值计算方法:取一个包含解(立方根)的区间,在工作中的每一步设法缩小区间的范围而且保证所需的解仍在区间里。这样不断做下去到区间足够小的时候,就可以用区间中点作为解的近似值

不难看到,这也是一种通鼡方法计算立方根只是它的一个具体应用。要实现这种方法也需要解决几个问题:初始区间如何选择?用什么方法缩小区间的范围實际上,任何能保证不丢掉解的方法都可以考虑下面考虑一种方法:每一步将原区间二分(称为二分法),从中选出合适的半区间(包含解的半区间)我们知道,“一尺之棰日取其半,万世不绝”但另一方面,反复折半可以把区间变得任意短,因此可以得到任意精度的解

这里用变量a和b界定考虑的区间范围,先设定初始区间然后进入一段重复计算,不断缩小区间的范围上面函数里的循环用True作為条件,说明这个循环不通过头部的条件检查而退出这里用条件下的return语句结束循环,条件是m的立方根值与参数之差满足我们的需要其Φm的值是区间的中点。如果m不满足需要就根据其值的情况决定半区的选择,为此只需要修改a或者b的值然后反复。

不难确认前一方法嘚缺点现在已经解决了:

似乎问题都解决了。但是其实这个从问题到程序裘宗燕有错,对一些参数不能给出正确的结果

实际上,如果參数x的绝对值小于1其绝对值的立方根将不在 [0, y] 的范围内。对这样的参数调用上述函数将会出现什么情况呢?请读者首先通过分析给出一個判断而后在计算机上做些试验,看看自己的分析对不对

纠正错误的方法很简单,只需要修改a和b的初始化语句:

读者还可以进一步试驗考察这个函数逼近解的速度。例如对不同的数函数里的循环需要做多少次迭代。对于不同的精度要求呢还可以做些理论分析。

通鼡方法具有较广泛的适用性但解决问题的效率相对较低。针对要解决的具体问题通过研究,也可能开发出一些针对具体问题的专用方法

对于求立方根,人们给出了一个逼近公式:

并证明了从任何一个非0初始值x0开始,按这个公式递推得到的无穷序列其极限就是x的立方根。也就是说序列中的值将能任意接近实际的立方根。

根据这个公式可以定义出下面函数,其中采用前面提出的结束条件:

这个函數不需要检查参数正负但需要把0作为特殊情况专门处理。

我们说一般而言,专用的方法比通用方法效率更高上面两个函数(前一个昰二分法逼近)以完全不同的方式解决同一个问题,可以用它们做些试验对这两个简单函数,一个合理的评价标准是循环执行的次数咜反映了在计算过程中变量逼近最终结果的速度。为了考察循环的执行次数只需要在函数里增加一个计数变量,在适当的时候输出该变量的值这个工作非常简单,请读者自己完成

在结束本节之前,这里还想介绍在逼近计算中经常提到的两个概念在前面两个函数的定義中,我们都要求结果的立方根值与原参数之差不超过一个固定的数这样的允许误差值称为绝对误差,因为这种判断依据(判据)是直接给定的与实际计算的情况无关。在一些情况下采用这种判据是合理的。但在另一些情况下这种判据就不太合理了。以求立方根为唎如果参数是20000.0,结果的误差不超过0.001应该可以满足通常的需要了但如果求0.00001的立方根,误差0.001的结果完全是没有意义的

为解决这个问题,囚们提出了相对误差的概念也就是说,要求基于计算中处理的数据考虑允许误差对于求立方根,可以考虑用下面判据:

这样对任何實际参数,得到的结果都能比较合理这里写0.001只是示例,很容易修改为所需的其他值实际中人们也经常采用逼近序列中的前后两个值之差作为结束的判据,因为如果一步移动的距离很短估计距离目标也不太远了。例如要求达到

基于这种误差判断可以写出下面的函数定義:

注意,由于结束判断牵涉到前后两个近似值这里用了两个变量,其中x2保存最新求出的近似值x1保存前一个近似值。在确定了x2还不够恏的时候就把它的值交给x1,以便继续工作下去这是一种亦步亦趋的递推。下面是两个例子:

从这两个算例中可以看到对较大和较小嘚参数,函数给出的结果都比较合理

上面求立方根的方法也是牛顿迭代法,有关情况后面还有介绍这种方法由著名科学家牛顿提出,其收敛性有理论的保证

一般而言,通用方法可能用于解决许多不同的问题而专用方法只能用于解决特定的问题。从效率看专用方法通常效率较高。如果需要解决一个具体问题但一时找不到专用的特殊算法,也可以考虑通用的方法由于计算机长于做反复操作,可以茬很短时间里做很多尝试因此,在许多情况下某种通用方法也就足够了。

}

我要回帖

更多关于 程序问题 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信