maplechap5Word文件下载.docx
- 文档编号:21496843
- 上传时间:2023-01-30
- 格式:DOCX
- 页数:20
- 大小:228.32KB
maplechap5Word文件下载.docx
《maplechap5Word文件下载.docx》由会员分享,可在线阅读,更多相关《maplechap5Word文件下载.docx(20页珍藏版)》请在冰豆网上搜索。
参数->
函数表达式
而对于无参数的函数或者多变量函数,箭头操作符的用法是:
(参数序列)->
无参数函数也许不好理解,它可以用来定义常值函数,我们通过一个例子一看就可以明白:
还可以看出,Maple对于多余的参数采取的是置之不理的态度,而不像通常的编程语言一样有严格的检查,因此需要格外引起注意。
5.2最简单的子程序
在前一节中,我们介绍了箭头操作符,利用它可以生成最简单的函数——单个语句的函数。
严格意义上来讲,它还不能被称作程序设计,但它所生成的数据对象是子程序(procedure)。
简单地说,一个Maple子程序就是一组预先编排好的命令。
我们先由一个最简单的子程序来看看Maple子程序的结构:
这个子程序只有一个参数,在子程序内部它的名称是x。
在这个程序中,我们计算了x/2的浮点近似值,由于这条语句就是该子程序的最后一条语句,所以该子程序就返回算得的近似值。
上面的子程序是Maple最简单的子程序结构,仅仅在proc(…)和end中加入我们在预算步骤中需要的一条或者多条命令就可以了。
Maple会把最后一个语句的结果作为整个子程序的返回结果。
我们再来实现稍稍复杂一点的功能,例如,我们要完成下面的计算任务:
我们需要把它编制成Maple子程序(虽然这简直是杀鸡用牛刀),还是用上面的方法,把它们放在proc和end的中间。
我们解剖麻雀,从这个简单的例子中来看看Maple子程序中都有些什么:
✧我们把定义的子程序赋值给f,以后我们就可以用子程序名f来调用它了。
✧子程序一律都以proc()开头。
括弧中什么也没有,表示这个子程序没有任何输入参数。
✧子程序中的每一个语句都用分号隔开(当然也可以用冒号,结果是相同的)。
✧在定义完子程序之后,Maple会显示它对于该子程序的解释(除非在end后用冒号结尾),它的解释和你的定义是等价的,但并不一定完全相同。
✧Maple会自动地把除了参数以外的变量都作为局部变量(localvariable),这就是说,它们仅仅在这个子程序的定义中有效,和子程序以外的任何同名变量毫不相干。
有关局部变量和全局变量,我们将在下一节中详细介绍。
由于子程序的定义比较长,一般不能在一行之内输入完,换行是,为了避免出现“语句未完”的警告,可以使用Shift+Enter换行。
在定义了一个子程序之后,我们当然要执行它,执行一个子程序的方法和执行任何Maple的系统子程序完全一样——程序名再加上一对括弧,其中包含着调用的参数。
即使子程序没有参数,括弧还是不能省略的。
我们曾经说过,和Maple中的数组、映射表一样,Maple的子程序也不能自动地求值。
如果需要察看一个已经定义好的子程序的具体定义,可以使用我们接触过多次的eval命令:
5.3局部变量和全局变量
Maple中的全局变量,是指那些在交互式命令环境中定义和使用的变量,比如我们在前面几章中用到的所有变量都是全局变量。
而在编写子程序时,你会需要定义一些变量,这些变量只在子程序内部使用,我们称其为局部变量。
当Maple执行子程序时,所有和局部变量同名的全局变量都保持不变,而不管在子程序中给局部变量赋予了何值。
前面我们已经知道,在子程序中,变量默认情况下都是局部变量。
如果要把它定义成全局变量,需要用关键字global在程序最开始加以申明;
对应的,局部变量也可以用local加以申明,虽然这不是必要的,但申明变量是一个好习惯。
所谓变量的作用域,是指所有可以对该变量操作的子程序和语句的集合,对于简单子程序(无嵌套的子程序)来说,作用域无外乎两种——要么在一切地方都有效(全局变量),要么仅仅在一个子程序中有效(局部变量)。
对于嵌套子程序,情况要稍稍复杂一点,我们将在后面详细介绍。
我们通过两个例子来演示局部变量和全局变量的不同。
首先,为了观察子程序对全局变量的影响,我们在子程序外设定一个变量的值:
接着,我们分别定义两个子程序。
第一个,定义a为其局部变量:
然后执行它,再来看看外面的变量a的值。
可以看到,a没有受子程序中同名局部变量的干扰:
然后再试试把程序中的a定义成全局变量:
可以看到,外面的a也跟着程序中的a变了。
不过,像这样地使用全局变量当然没有什么实际意义;
而在实际程序设计中,全局变量有时还是必不可少的,例如在子程序中需要输入或输出大量的数据时,仅依靠参数和返回只有时显得力不从心,这是使用全局变量就可以在一定程度上带来方便。
在子程序设计中更重要的一类变量是子程序的输入参数,它既不是全局的,又不是局部的。
对于子程序内部,它是形式参数;
也就是说,它的具体取值尚未被确定,它的出现,将来在调用时都会被替换成真正的参数值。
而对于子程序外部,它们仅仅表示该子程序接受的参数的多少,而对于具体的参数值毫无关系。
5.4基本程序结构
几乎任何一种高级语言都或多或少地具有程序结构。
因为为了完成一项复杂的任务,仅仅按照语句顺序依次地执行是远远不够的。
我们需要程序在特定的地方能够跳转、分叉、循环……,与此同时,程序结构就应运而生了。
这一节中,我们将依次介绍Maple语言中的基本程序结构。
5.4.1for循环
在程序设计中,我们常会遇到这种情况,需要把相同或者类似的语句连续执行多次。
举个例子来说吧,比如我们可以这样计算前5个自然数的和:
显然这是一个笨办法,我们可以通过for循环结构更便捷地编写这段程序:
Maple的for循环结构很接近于英语语法,所以我们很容易看懂上面例子的意思。
例子中的i,我们称它为循环变量,是用于循环计数的。
上面的例子中i从1变到5,总共执行了5次。
Do后面的部分,称为循环体,它就是需要反复执行的语句,可以是一条语句,也可以是多条语句。
在循环体中,我们也可以引用循环变量。
循环体的结束标志,也就是整个for循环结构的结束标志,是两个字母——od——很奇怪,它不是一个英语单词——但它比英语单词更好理解,既然循环体的开始标志是do,那么它的结束标志就是把do反一反,od是也;
就像C语言中的一对花括弧,对称地将循环体包在中间。
类似于这样界定程序块的方法,我们在其他的结构中也能看到,这也许是Maple的一个特色吧。
我们可以把for循环结构的语法总结如下:
for循环变量[from初始值]to终了值do
循环体
od
for循环的初始值和终了值必须都是数值型的,而且必须是实数。
如果循环初始值是1,那么就可以省略不写。
对于这5次的重复,循环似乎可有可无;
但实际的科学计算中,重复次数往往成千上万,或者重复的次数尚未确定,循环就必不可少了。
比如,我们可以编制一个求前n个自然数的和的子程序:
作为实验,我们用这个子程序来计算一下前100个自然数的和:
完全正确。
当然了,对于这样的简单问题,没有必要兴师动众编制程序,完全可以利用Maple函数更简单地完成:
5.4.2分支结构
另一个重要的基本程序结构是分支结构,分支结构是程序在执行时,依据条件的不同,分别执行两个或多个不同的程序块,所以我们又常常把它称作条件语句。
这种结构在数学中更是屡见不鲜,比如我们要计算一个变量的绝对值,就必须对它作判断,而依据判断的结果分别处理。
作为分支结构的简单例子,我们把这个函数简单地实现一下,为了防止和Maple的内部函数混淆,我们将它命名为ABS:
我们的绝对值函数的简单实现,对于计算实数范围内的数值型变量是没有问题的:
但是,这个函数对于非数值型的参数却是无能为力了:
从错误信息中我们看到,Maple在执行子程序ABS时发生了错误——无法对分支结构中的布尔表达式“x<
0”求值,所以Maple无法决定下面执行哪个程序段,程序被迫中断了。
这不是我们编制程序的本意,特别是对于Maple这个符号演算系统的编程来说,这种情况不是我们想看到的。
对于符号变量a,我们期望的结果是ABS(a)。
我们知道,可以用延迟求值操作符“'
”得到这样的表达式结果。
我们还可以再用一次条件语句,来判断参数是否为数值的变量:
这里,我们用到了一个if语句的嵌套,也就是一个if语句处于另一个当中;
这样的结构可以用来判断复杂的条件。
我们还可以利用多重嵌套的条件结构来构造更为复杂的函数,例如下面的分段函数:
尽管我们用了不同的缩进来表示不同的层次关系,这段程序还是很难读懂。
对于这种多分支结构,我们可以用另一种方式来书写。
我们可以在第二层的if语句中使用elif(elseif),这样,就可以把多个分支形式上写在同一个层次中,以便于阅读。
例如上面这个程序,就可以改写为更明了的形式:
和许多高级语言一样,这种多重分支结构理论上可以多到任意多重。
我们再回过头去,看看我们在前面定义的绝对值函数,它似乎已经很完美了。
但是,在作进一步的试验,我们又发现了它的缺陷,比如我们用它来求一个乘积的绝对值:
但我们知道,乘积的绝对值可以化成绝对值的积,即|ab|=|a||b|。
根据前面学过的,我们知道,可以用map函数来实现这一点:
我们可以用type(…,`*`)来判断一个表达式是否为乘积,进而将其化简。
利用多分支结构,我们把ABS子程序扩展如下:
这一改变对于计算含有数值乘积的绝对值特别有用:
5.4.3while循环
前面我们已经介绍过for循环,for循环在那些已知循环次数,或者循环次数可以用简单表达式计算的情况下比较适用;
但有时循环次数并不能简单地给出,我们要通过计算,判断一个条件决定是否继续循环,这时,可以使用while循环。
While循环有以下形式的标准结构:
while条件表达式do循环体od
Maple首先判断条件是否成立,如果成立,就一遍又一遍地执行循环体,直到条件不成立为止。
作为while循环的例子,我们来编制一个简单的程序,用辗转相除法计算两个自然数的最大公约数。
在上面这个程序的参数a、b后面,我们用两个冒号指定了输入的参数类型。
这样,Maple就会自动检查调用时的参数类型,并在类型不匹配时返回错误信息。
5.5递归子程序
正如在一个子程序中我们可以调用其他的子程序一样(比如系统内部函数,或者我们已经定义好的子程序),一个子程序也可以在它的内部调用它自己,这样的子程序我们称为递归子程序。
在数学中,我们常常可以看到递归定义的例子,比如菲波那契(Fibonacci)数列,就是用递归的方式定义的:
f0=0,f1=1,fn=fn–1+fn–2(n≥2)。
同样,在Maple中,我们可以利用递归子程序求菲波那契数列的第n项:
我们用它来求菲波那契数列的前20项:
不管你用的是什么样的计算机,上面的计算一定化了相当长的时间(当然是对计算机而言的,1~2秒已经是非常长的了)。
不妨用time来看一下计算f20所用的时间:
这是因为上面的程序把相同的计算进行了多次,比如计算f19时,需要计算f18和f17;
而计算f18时,又要计算f17和f16……。
这样,就等于计算了两次f17,三次f16,……。
如何才能避免这样的重复运算呢,我们可以让Fibonacci子程序记住已经计算过的结果。
可以用在子程序定义时在最前面加入optionremember,这样,Maple就会把计算过的结果记入该子程序的记忆表中,重新计算就会大大加快,就像大部分内部函数一样。
有关记忆表的内容,我们将在后面专门讨论。
下面是修改后的Fibonacci子程序:
这个版本的Fibonacci子程序速度比原来的提高了很多:
但是,没有事物是十全十美的,在速度上提高了,就一定会在其他方面有所付出,记忆表会占去宝贵的系统资源。
递归程序也是一样,它可以简化算法,使程序简明易懂;
但它也有不足之处,调用递归子程序受堆栈的限制,不可能无止境地调用下去。
比如,我们在MicrosoftWindows98环境下就不能用上面的子程序求得f2000。
所以,我们在编写递归子程序时一定要注意,不能无限制地递归,也就是说,递归必须要有一个结束;
否则,必然会耗尽系统的堆栈空间而导致错误。
从数据结构课上我们知道,递归程序理论上都可以用循环实现,比如我们可以用循环改写上面的Fibonacci程序。
利用循环,程序不如以前那么易懂了;
但同时带来的好处也是不可忽视的,循环结构的程序不仅不会受到堆栈的限制,而且计算速度也有了很大的提高:
我们知道,Maple子程序默认情况下把最后一条语句的结果作为子程序的结果返回;
然而我们也可以利用RETURN命令来显式的返回结果,比如前面的递归形式的程序,可以等价地写成:
程序进入n<
=1的分支中,执行到RETURN命令就跳出程序,而不会接着执行后面的语句了。
使用RETURN命令在一定程度上可以增加程序的可读性。
5.6子程序中的求值
在Maple子程序中的语句,求值的方式和交互式环境中的求值有所不同。
这在某种程度上是为了提高子程序的执行效率而考虑的。
在交互式环境中,Maple对于一般的变量和表达式都进行完全求值(除了数组、映射表等数据结构外)。
比如你先将b赋给a,再将c赋给b,则最终a的值也指向c。
但在子程序内部,情况就不同了,我们将上面的语句写到一个子程序中:
运行的结果似乎令人吃惊:
这是因为a和b都是局部变量,Maple对于子程序中的局部变量只进行一层的求值。
下面,我们对于子程序中不同的变量,系统地介绍子程序中的求值机制
5.6.1参数
子程序的参数,就是那些在proc()的括弧中的变量,我们前面说过,参数试一类特殊的变量,在调用子程序时,会把它们替换成实际的参数。
我们来看下面的程序,它把第一个参数的平方赋给第二个参数(第二个参数的类型是name,也就是说必须是未被赋值的变量)。
我们再来试试别的参数,比如用我们在前面赋值过的a作为第一个参数;
第二个参数还用ans,但这次需要加上单引号了(想想为什么)。
看来,Maple清楚地记得a被赋值为b,而b被赋值为c。
这里,显然Maple对参数a进行了求值,但是,这个求值究竟发生再生么时候呢?
我们用调试工具stopat让程序在进入sqr1时停止。
这时候程序中的形式参数x已经是c了。
我们可以在调试提示符DBG>
下键入需要察看的变量名x:
看来,Maple在进入子程序以前就已经对参数进行了求值了。
因为是在子程序外进行求值,所以求值规则服从调用是所在的环境。
上面是在交互式环境下调用sqr1,所以得到的是c;
作为对比,我们来看看如果在程序内部调用上面的sqr1子程序会有什么不同。
因为这次调用是在程序内部,所以只进行一层求值,进入sqr1时参数值为b。
Maple对于子程序的参数,都只在调用之前进行一次求值,而在子程序内部出现的地方都用这次求值的结果替换,而不再重新进行求值。
比如我们这样修改前面的子程序sqr1:
可见,对参数赋值的作用只有一个——返回一定的信息——因为在子程序内部,永远不会对参数求值。
我们可以认为,参数是一个0层求值的变量。
5.6.2局部变量
正如我们在前面所见,Maple对于局部变量只进行一层的求值,也就相当于用eval(a,1)得到的结果。
这种求值机制不仅可以提高效率,而且还有着更重要的作用。
比如在程序中,我们往往需要把两个变量的值交换,一般我们使用一个中间变量来完成这样的交换。
但如果求值机制和交互式环境中一样,将达不到我们的目的。
不妨试一试,在交互式环境下,我们企图用这样的方法交换两个未被赋值的变量会有什么后果:
5.6.3全局变量
不管是在交互式环境下,还是在程序中,Maple对于每一个全局变量都进行完全求值,除非它是数组,映射表,或者子程序。
对于数组,映射表和子程序,Maple采用赋值链中的上一名称来求值。
除了上面这些特殊数据对象还有下面将有介绍的两个特例外,总结起来,Maple对子程序的参数进行0层求值;
对于局部变量进行1层求值;
而对于全局变量,则进行完全求值。
5.6.4特例
对于上面说的求值规则,在Maple中还有两个特例,需要引起注意。
其一是“同上”操作符“%”,就其作用域来说,它应该属于局部变量。
在进入一个子程序时,Maple会自动地把该子程序中的%设置成NULL(空)。
但是,对于这个“局部变量”,Maple不遵循上面的规则,无论在哪儿,都会将其完全求值。
在交互式环境中当然无可非议了,我们下面通过一个例子说明它在子程序中的求值机制:
我们看到,尽管在子程序内部,局部变量a仅进行一层求值,但指代a+1的同上操作符却进行了完全求值,得到d+1的结果。
第二个特例是环境变量。
所谓环境变量就是像Digits这样的变量。
从作用域来看,它们应该算作全局变量。
在求值上,它们也像其他全局变量一样,始终都是完全求值。
但是如果在子程序中间对环境变量进行了设置,那么在退出改子程序时,系统会自动地将其恢复成进入子程序时的状态,以保证当前运行时的环境。
正因为此,我们称其为环境变量。
我们通过一个简单的例子来说明这一点:
我们知道,默认情况下Digits的值是10:
而从子程序g中返回时,Digits又被自动地设成了原来的值。
如果你需要自己定义一些具有这样的特性的环境变量,也十分简单——Maple会把一切以_Env开头的变量认作是环境变量。
5.7嵌套子程序
在很多情况下,我们可以在一个子程序的内部定义另一个子程序;
实际上,我们在写这样的程序时常常没有意识到它是嵌套子程序。
用于对每一个元素操作的map命令我们在交互式环境下已经非常熟悉了。
再程序设计中,这一命令也相当有用。
比如我们要编写一个子程序,它返回有序表中的每一个元素都被第一个元素除的结果。
我们不知不觉中已经在子程序nest中间定义了另一个子程序:
y->
y/v。
这个子程序中有一个变量v,Maple根据有效域的范围,认为它就是外面的子程序nest中的同名变量v。
那这是一个全局变量呢,还是一个局部变量?
显然,两者都不是。
如果把上面例子中内部子程序的v申明成local或者global,都无法达到我们的目的。
那么,Maple究竟是怎样来判定一个变量的作用域的呢?
首先,它自里向外,一层一层地寻找显式地用global、local申明的同名变量,或者是子程序的参数。
如果找到了,它就将其绑定在外层的同名变量之上,实际上就是将两个变量视为同一,就像上面例子中的情况。
如果没有找到,就遵循下面的原则:
如果变量位于赋值运算符“:
=”的左边,就视其为局部变量;
否则均认为它是全局变量。
5.8记忆表
有时候,一个子程序会被多次调用,而且调用的参数也相同;
那么,Maple也会不厌其烦地一边有一边地计算同一个答案。
正如前面所介绍过的,可以利用记忆表来改进这一点。
每一个Maple子程序都可以有记忆表,记忆表的目的是为了提高计算效率,它把每一个计算过的结果存储在一个映射表中,所以在下一次用相同的参数调用的时候可以避免重新计算。
Maple的记忆表是哈希表,索引速度非常快。
但由于多种不同的参数调用可能导致记忆表变得很大,所以记忆表适合于那些常常需要计算相同结果,而就算过程有相当复杂的子程序。
记忆表的用法有几种,我们在下面分别加以介绍:
5.8.1remember选项
在定义子程序时,我们可以加入remember选项,来建立该子程序的记忆表。
从本章第5节中递归形式的菲波那契数列子程序中,我们早已体会到了加不加这个选项有什么差别。
为了对记忆表加以说明,我们在这里再建立一个简单版本的Fibonacci子程序:
在我们用它计算了f3之后,它的记忆表中就有了4项。
我们可以用op命令来检查一个子程序的记忆表:
记忆表是子程序的第4个元素——我们用op(4,…)来获得;
这里顺便题一句,一个子程序的前三个元素分别是它的参数、局部变量、和选项(比如option)。
5.8.2在记忆表中加入项
通常,调用具有记忆表的子程序并且计算得到结果,Maple会自动地把结果加入到记忆表中去;
但我们也可以手动地在记忆表中添加内容。
您也许还记得,在这一章的开头,我们曾经试图用f(x):
=expr的形式定义函数,但得到的却是函数的记忆表。
这就是记忆表项的添加方法。
我们可以利用记忆表,再次简化我们的Fibonacci子程序——甚至用不着分支结构:
当然,在使用之前,我们还需要在它的记忆表中添加菲波那契数列的初始项:
在适当的时候,巧妙地利用记忆表把我们已知的结果“告诉”Maple,可以大大地提高解决问题的效率。
5.8.3在记忆表中删除项
记忆表是映射表的一种,可以方便地在其中添加或者删除特定的项。
和一般变量一样,删除一个表项只需将其名称用evaln赋给它本身就可以了。
例如,因为输入错误,我们在Fibonacci子程序的记忆表中加入了一个错误的项:
由于映射表和数组一样,在赋值时不产生新的拷贝(参见第四章)。
我们可以将Fibonacci的记忆表先取出来,再将相应的
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- maplechap5