三、函数与递归
三、函数与递归 返回目录页
1、函数的嵌套调用
2、自定义函数
3、辅助函数
4、匿名函数
5、单行函数
6、递归
引用一个MMA粉丝一段话:
Mathematica支持很多的编程范式(有可能是最多的),其中最为高效的应该就是函数式了,熟悉一点函数式语言的人再来接触Mathematica可能会倍感亲切。通过纯函数(相当于Lambda演算)、高阶函数(Nest、Fold、Map、Apply等等)等各种函数式编程的技巧,你可以轻易写出简洁到爆的程序,而且绝大部分情况下都比过程式版本高效得多。
( 来源于知乎:https://www.zhihu.com/question/20324243 )
习惯于过程式编程的人,往往很不习惯这种函数式编程。开始的时候,这种反应是正常的。
我们学习函数式编程,主要是因为可以开阔自己的视野,给自己一个看问题的不同的视角。
-------------------------------------------------------------------------
1、函数的嵌套调用
几个函数的依次作用,被称作嵌套函数调用(nested function call)。
因为函数的返回值,可以作为另一函数参数来用,所以函数可以嵌套调用,一层又一层。
我们学习编程,一般以阅读人家的程序开始。
开始来读懂这个嵌套函数:
Plus[Power[x,3],Power[Plus[1,x],2]]
----------------------------------------
树形结构。
在MMA中,可以将任何一个表达式看作一个树。
函数作为表达式,也是一个树。
树的呈现方式有很多。一般常用的,有两种。
第一种,当然是画出树的图形。用TreeForm函数,很容易画出。
TreeForm[x^3 + (1 + x)^2]
得:一个树形图。
第二种,以一种缩进的方式表达树。
one
two
twoL
twoR
three
tL
L
R
tR
根节点是one,根节点下有两个分支:two和three
two节点下,有两个分支:twoL和twoR
three节点下,有两个分支:tL和tR
tL节点下,有两个分支:L和R
那么,上面以第一种方式得到的树形图,可以用第二种方式来表示:
Plus
Power
x
3
Power
Plus
1
x
2
标准计算过程采用深度优先方式遍历表达式树。
如果没有学过数据结构,这话听起来有点玄。没有关系,很好理解:
嵌套函数在计算时,从最内层函数开始,一层层向外层函数进行。
那啥叫内层、啥叫外层呢?
----------------------------------------
代码风格。
在MMA中,很多嵌套函数是一行表示的。比如:
Plus[Power[x,3],Power[Plus[1,x],2]]
很多时候,换行会使代码更易读:
Plus[
Power[
x,
3
],
Power[
Plus[
1,
x
],
2
]
]
这种以分别两个空格缩进的方式,使代码与树形结构的第二种表达方式很相似了。
如果把[]与逗号去掉,就一模一样了。
所谓内层,指靠近树叶位置的。所谓外层,指靠近树根位置的。
内层先算,得到结果给外层。算到树根,计算结束。
(这不是就是递归运算么?是啊,MMA中大量使用递归运算。)
一个坑爹问题。
上面的缩进代码,Copy到MMA的笔记本中去时,缩进自动完全消失。变成一行了。
一直在找一个合适的MMA代码编辑器,一直没找到。
一个好消息。
当在笔记本中鼠标点击代码的不同部分时,外层函数头与[]会自动着色。
这个对阅读代码很有好处。着色的颜色,可以在菜单 (编辑/偏好设置) 中设定。
MMA自带的编辑器(即笔记本),有自动完成功能,有自动缩进功能,还不算太差,先这么用着吧。
----------------------------------------
分清楚内层外层是第一步。
然后呢,就是按步构造法,就是搞清楚每一步的函数的基本功能。
参数有几个呀、函数的实现功能是啥呀。一步步进行,最后就完全懂了。
Plus[Power[x,3],Power[Plus[1,x],2]]
Plus干啥的?参数有几个?
嗯,如果碰到不太熟悉的函数,鼠标在函数中间任意点点击一下,按“F1”。
这没啥高深的道理,这是一种技能,越用越熟。
----------------------------------------
我们来看一个纸牌程序。先创建,再洗牌。
la = Join[Range[2, 10], {J, Q, K, A}] (*得到十三张牌,没花色的*)
得:{2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A}
这里的la,是值的名称,是个符号,是个绰号,是个别名(nickname)。以后不管它出现在什么地方,都将被这个值本身所取代。
就是说,后面老是用长长一串:{2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A},麻烦不?
直接用la来代表就可以了。
用完之后,可以把一个值从这个名称上去除掉,两种方法中选取任一种均可:
Clear[la]
la=.
lb = Outer[List, {c, d, h, s}, la]
最后的la,就是{2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A},没有任何区别。
但代码好读了呀。
从得到的结果来看,这个表的层数太多了。
我们希望得到的,是这样一个表:{ {c,2},{d,A},... },作为52张牌的数据。
任何一张牌,都可以这样表示:{a,b}。a指花色,b指牌的大小。
层数太多,用Flatten函数来压平好了,指定层数为1:
Flatten[lb, 1]
52张牌的数据就这么愉快地得到了。
我们把la、lb去掉,都用本身,不用绰号,那么就变成了一句:
lc = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1]
当然,输出结果是一样的。
但这个嵌套函数有点长,不易读。
这里我们学到了解读嵌套函数的又一招:用绰号。
Flatten[Transpose[Partition[lc, 26]], 1]
(*Partition是把52张牌一分为二。Tran...是进行转置,即化列为行。最后压平。洗牌完毕*)
如果我们不用绰号,整个洗牌程序就是这么长长的一串:
Flatten[Transpose[Partition[Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1], 26]], 1]
你可能会觉得,这个洗牌程序洗出来的牌,太有规律了,不够乱。不急,这副牌会跟随我们很长时间,以后还会不断玩牌。
从这一节,我们看到,函数式编程的第一大特点:啥都是函数,函数套函数,层层叠叠。
不过,我们已经掌握了几种解读嵌套函数的技巧。
-------------------------------------------------------------------------
2、自定义函数
MMA中的内置函数虽然很多,但用户的需求是无穷多的,很多时候必须自定义(user-defined)函数。
比如发牌程序,就不是内置函数。
自定义函数的一般格式:
name[arg1,arg2,...,argn] := body
依次为函数名(function name),函数参数(gargument),函数主体(body)
特别的一点是,函数参数必须以下划线(blank)结尾,比如:x_
因为内置函数以大写字母开头,所以一般我们取自定义函数名的时候,就不以大写字母开头了。
square[x_] := x^2
square[3]
函数经过自定义,就可以像内置函数一样使用了。
----------------------------------------
准备发牌。
cardDeck = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]]
第一行不用解释了。第二行,自定义了一个函数,函数功能是从一个表中,随机删除一个元素。
RandomInteger[{1, Length[lis]}]
产生一个1到表长度中的一个随机整数。
cardDeck = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]];
removeRand[cardDeck]
调用函数后,就有了一个表的输出。能够发现少了哪张牌么?
真的去数了么?哈哈,恭喜你,上当了。
一般玩程序的,能用程序解决的,不会去干手工。
Complement[cardDeck, %]
加一句,少的那张牌就出来了。
奇怪啊,已经删除了,怎么出来的?
Complement有取补集的功能。把cardDeck看成全集,把%中部分的51张牌,看成是一个子集,那么补集就是删除的那张牌了。
发n张牌,就这样写:
deal[n_] := Complement[cardDeck, Nest[removeRand, cardDeck, n]]
其中,Nest[removeRand, cardDeck, n] 的意思是,不断在剩余的牌中随机删除一张,直到删除了n张。
发5张牌全部程序就是:
cardDeck = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]];
deal[n_] := Complement[cardDeck, Nest[removeRand, cardDeck, n]];
deal[5]
Map[deal, Table[5, {4}]] // MatrixForm
这样就给四个人发了五张牌
Map[deal, Table[2, {6}]]
deal[5]
那就是六个人在玩德州梭哈,每个人发两张牌。然后,在中间发五张牌。
发现木有?我们在这章到现在使用的内置函数,全部都是在前一章表操作函数中学过的。
-------------------------------------------------------------------------
3、辅助函数
辅助(auxiliary)函数,可以理解为自定义函数嵌套,即自定义函数的函数体内,还有自定义函数。
分两种格式:复合函数(compound function)与Module。
复合函数的基本格式:
name[arg1,arg2...,argn] := (expr1; expr2; ... ; exprm)
函数体在()中,只有最后一个表达式exprm有输出。
把前面的发牌程序改一下,就成这样:
Clear[deal, cardDeck, removeRnad];
deal[n_] := (
cardDeck = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]];
Complement[cardDeck, Nest[removeRand, cardDeck, n]]
)
deal[5]
程序可读性增加了。但是,cardDeck之类的名称,还是全局可见的,所以这种格式不常用。
而以下的Module格式,把cardDeck之类的名称,作为局部的,全局不可见,所以就比较常用了。
name[arg1_,...] := Module[{name1,name2=value,...}, expr]
Module中的第一个参数,是个表。表中就是我们想要把它们的名称局部化的表达式。
Clear[deal, cardDeck, removeRnad];
deal[n_] :=
Module[{removeRand, cardDeck},
cardDeck =
Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]];
Complement[cardDeck, Nest[removeRand, cardDeck, n]]]
deal[5]
把名称,通过Module格式局部化,经常是个好主意。
局部化名称的颜色,设置得显眼一点,也是个好主意。在菜单 编辑/偏好设置 中设置。
-------------------------------------------------------------------------
4、匿名函数
匿名函数(anonymous function)的名称可多了,无名函数,纯函数(连名字也没了,确实比较纯:))
匿名函数的特点是,它没有函数名。
一般我们说调用函数,先写上函数名。匿名函数没有函数名,所以是当场使用,一次性。
定义匿名函数,有两种方式。
第一种是使用内置函数Function:
Function[{x,y,...}, body]
Function[x,x^2] [3]
得9
创建函数后,马上用掉,一次性。对于以前反复使用大茶缸的,现在使用一次性茶杯,不习惯是正常的。
用多了就习惯性了。如果想不一次性,给匿名函数起个名,那也是可以的:
square = Function[x, x^2];
square[3]
得9
第二种是使用语法糖:
(#1,#2...)&
反正看到这种模样:(...)& ,就要想到,这是个无名函数。记住:()&,这三个字符,是个整体。
当参数只有一个时,可以使用#。当然了,使用#1也是可以的。
(#^2)&[3]
(#1^2)&[3]
这两句是等效的。
以这种定义方式,给无名函数起个名,也一样是可以的:
square = (#^2)&;
square[3]
一些简单的自定义函数,通过这种方式定义,还是简洁可行的。不过一般不这样做。
无名函数的主要功能是一次性。
无名函数作为函数,当然也可以嵌套使用。
(Map[(#^2)&, #])& [{1,2,3}]
表达式的运算过程,是从内层到外层。但解读程序时,很多时候从外层到内层,能抓住整体性。
(Map[(#^2)&, #])& 这是一个函数。
[{1,2,3}] 这是函数参数。
(Map[(#^2)&, #])& 这个函数,把()&部分剥离,得:
Map[(#^2)&, #]
逐渐清晰,这是个Map函数,功能是把某函数((#^2)&)分别作用于某表(#)。
注意啊,以上两个#,所代表的含义完全不同。
第一个#,是函数(#^2)&的参数。第二个#,是函数Map的参数。
使用第一种方式来写,会不会清楚点呢?
Function[y,Map[Function[x,x^2],y]] [{1,2,3}]
写一起,比比看:
(Map[(#^2)&, #])& [{1,2,3}]
Function[y,Map[Function[x,x^2],y]] [{1,2,3}]
----------------------------------------
用无名函数来发牌。
以前的自定义函数:
Clear[deal, cardDeck, removeRnad];
deal[n_] :=
Module[{removeRand, cardDeck},
cardDeck =
Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]];
Complement[cardDeck, Nest[removeRand, cardDeck, n]]]
deal[5]
改写成匿名函数:(*无非是去掉个函数名:removeRand*)
Clear[deal, cardDeck, removeRnad];
deal[n_] :=
Module[{cardDeck},
cardDeck =
Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
Complement[cardDeck, Nest[(Delete[#, RandomInteger[{1, Length[#]}]])&, cardDeck, n]]]
deal[5]
个人感觉,这种改写,意义不大。
从一个表中,随机不重复地选择几个元素出来,这程序比较有实用性:
chooseWithoutReplacement[lis_,n_] :=
Complement[lis, Nest[(Delete[#, RandomInteger[{1, Length[#]}]])&, lis, n]];
chooseWithoutReplacement[Range[10],5]
上面的程序,已经没有必要用Module了,因为自定义的名称木有了,只有一些无名函数、内置函数等等。
具有这种形式的函数,称为单行函数(one-liner)。
这一节,以做出一个完全二叉树作为结尾。
Nest[{#, #} &, x, 3] // TreeForm
-------------------------------------------------------------------------
5、单行函数
单行函数,可以理解为无名函数的一个运用。
一个自定义函数一个语句解决。
关于约瑟夫问题,是指n个人围成圈,从第一个开始绕圈数1到m(这里m为2,即间隔数)。
不断数,每次数到m的人出局,一直到剩下最后一个人。
这里用表及表处理,作了模拟(让整个圈子的人转动,这点有点新意)。还给出了过程:
survivor[lis_] :=
Nest[(Rest[RotateLeft[#]]) &, lis, Length[lis] - 1]
(*RotateLeft是向左转圈。Rest是把表中第一个元素去掉。Nest是不断重复,结果是参数。最后一个参数是重复次数*)
survivor[Range[10]] (*调用函数*)
TracePrint[survivor[Range[10]], RotateLeft]
(*第二个参数,是TracePrint的参数。只跟踪RotataLeft相关的数据。。*)
-------------------------------------------------------------------------
6、递归
递归是这样一种函数:在自定义函数时,函数体内用到函数名本身。
递归在MMA内部大量使用,因为表达式的内部存放形式是树形,而递归是遍历树形的最通常的方式。
树的层数不多时,递归的效率是高的。反之递归的效率极低。
我们从斐波那契数(Fibonacci number)开始。
Fib是指这样一种数列:从0、1开始,后面的所有项,都是前两项之和。
f[n_] := f[n - 2] + f[n - 1];
f[6]
运行这个程序,可以看到警告:超过1024的递归深度。
因为没有递归基。
在递归的过程中,函数不断调用自己,总要碰到一个不需要递归便可计算出来的值,作为返回。否则就一直调用自己,停止不下来了。这个可确定计算出来的值,称为递归基。在Fib中,递归基是开始的两个数,0和1。
f[0]=0;
f[1]=1;
f[n_] := f[n - 2] + f[n - 1];
f[6]
确定递归基后,程序能正常运行了。
这个程序的内部结构是个二叉树,存在大量的重复计算,效率是极低的。
可以考虑用一种叫动态程序设计的方法,把中间结果保存下来。
f[0] := 0
f[1] := 1
f[n_] := f[n] = f[n - 2] + f[n - 1]
f[2000]
(*
其实啊,这只是举例。真正要提高算Fib的效率,直接迭代效率最高。
fib[n_] :=
Module[{a = 0, b = 1, c = 1, i = 2},
While[i < n, a = b; b = c; c = a + b; i++];
c]
fib[5]
把5改成50000试试?一样很快。
*)
----------------------------------------
很多表处理函数,也可以用递归实现。
比如:
length[lis_] := length[Rest[lis]] + 1
length[{}] := 0
length[{1, 2, 3}]
这里写成length,以示与内置函数Length的区别。
----------------------------------------
我们把发牌程序,写成递归形式。
原来的:
deal[n_] :=
Module[{removeRand, cardDeck},
cardDeck = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
removeRand[lis_] := Delete[lis, RandomInteger[{1, Length[lis]}]];
Complement[cardDeck, Nest[removeRand, cardDeck, n]]]
deal[5]
递归的:
cardDeck = Flatten[Outer[List, {c, d, h, s}, Join[Range[2, 10], {J, Q, K, A}]], 1];
deal[0] := {}
deal[n_] := Module[{dealt = deal[n - 1]},
Append[dealt, Complement[cardDeck, dealt] [[RandomInteger[{1, 53 - n}]]]]]
deal[5]
递归基是空表,因为啥牌也没有发。
很多时候啊,我们不必知道递归细节。但要知道思路。
一般的设计递归程序的思路是,假设已经完成了n-1步,那么第n步怎么办?
这里,我们假设已经发了n-1张牌。
dealt是个局部名称,记录了所发的n-1张牌的表。
在剩下的牌中,随机取一张,添加到n-1张牌中去,那么n张牌就发好了。
Complement[cardDeck, dealt] :剩下的牌
[[RandomInteger[{1, 53 - n}]]] :随机取一张。
牌共有52张,已经发掉了n-1张,所以剩下张数为:52-(n-1)=53-n。
----------------------------------------
最后,我们再来看一个递归程序,来结束本节、本章。
二叉树的基本单元,根节点记为“one”,左节点记为“oneL”,右节点记为“oneR”
那么我们用表来表示是:
{"one",{"oneL","oneR"}}
如果左右节点均有分支,那么表就不断嵌套。
我们来编制一个递归程序,来遍历这个二叉树表,而输出以缩进格式表示树结构,比如:
one
two
twoL
twoR
three
tL
L
R
tR
程序为:
----------------------------------------
printTree[t_] := printTree[t, 0] (*输出空,相当于没输出*)
printTree[{lab_}, k_] := printIndented[lab, 4 k] (*输出几个空格、内容*)
printIndented[x_, spaces_] :=
Print[Apply[StringJoin, Table[" ", {spaces}]], x]
printTree[{lab_, lc_, rc_}, k_] :=
(
printIndented[lab, 4 k]; (*输出几个空格、内容*)
Map[(printTree[#, k + 1]) &, {lc, rc}]; (*递归,遍历子树。这个程序只能处理二叉树*)
)
printTree[{"one", {"two", {"twoL"}, {"twoR"}}, {"three", {"tL", {"L"}, {"R"}}, {"tR"}}}]
----------------------------------------
printTree[t_] := printTree[t, 0] (*输出空,相当于没输出*) printTree[{lab_}, k_] := printIndented[lab, 4 k] (*输出几个空格、内容*) printIndented[x_, spaces_] := Print[Apply[StringJoin, Table[" ", {spaces}]], x] printTree[{lab_, lc_, rc_}, k_] := ( printIndented[lab, 4 k]; (*输出几个空格、内容*) Map[(printTree[#, k + 1]) &, {lc, rc}]; (*递归,遍历子树。这个程序只能处理二叉树*) ) printTree[{"one", {"two", {"twoL"}, {"twoR"}}, {"three", {"tL", {"L"}, {"R"}}, {"tR"}}}]
总的思路是,把二叉树图形,转化为表,再用自定义函数,把表转化为缩进格式的文本。
TreeForm[x^3 + (1 + x)^2]
这个程序产生的二叉树图,转化为表:
lis={"Plus", {"Power", {"x"}, {"3"}}, {"Power", {"Plus", {"1"}, {"x"}}, {"2"}}}
printTree[lis]
调用函数,得:
Plus
Power
x
3
Power
Plus
1
x
2
从而我们看到了二叉树的不同表达形式。
++++++++++++++++++++++++++++++++++++++++++
扩展阅读:《计算机程序的构造和解释(SICP)》,传说中的MIT教程,函数式编程必读书。
豆瓣上的评价(链接)
这本书的中文版翻译得很好(作者叫裘宗燕,听起来是个女士,实际上是个大男人:))。这本书中,用的是Scheme语言,不是伪代码。下载一个PLT Scheme,就可以玩Lisp方言Scheme了。(这本书的中文版的pdf,及玩PLT Scheme的安装程序,均在目录页的百度云中提供下载。)
知乎上的讨论(链接)
Top