• 三、函数与递归



    三、函数与递归                                  返回目录页

    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"}}}]
    View Code


    总的思路是,把二叉树图形,转化为表,再用自定义函数,把表转化为缩进格式的文本。

    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










     

  • 相关阅读:
    cv2.imwrite()指定图片存储路径问题
    fgets读取文件最后一行重复问题
    KEAZ128 时钟配置
    MinGW x64 for Windows安装
    [python] pygame安装与配置
    S32K144之时钟配置
    C/C++ scanf和gets 区别 , printf和puts区别
    堆排序
    约瑟夫问题
    Coursera 国内无法登陆问题
  • 原文地址:https://www.cnblogs.com/xin-le/p/5992397.html
Copyright © 2020-2023  润新知