zoukankan      html  css  js  c++  java
  • 三、函数与递归


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

    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










     

  • 相关阅读:
    HDU 6071
    HDU 6073
    HDU 2124 Repair the Wall(贪心)
    HDU 2037 今年暑假不AC(贪心)
    HDU 1257 最少拦截系统(贪心)
    HDU 1789 Doing Homework again(贪心)
    HDU 1009 FatMouse' Trade(贪心)
    HDU 2216 Game III(BFS)
    HDU 1509 Windows Message Queue(队列)
    HDU 1081 To The Max(动态规划)
  • 原文地址:https://www.cnblogs.com/xin-le/p/5992397.html
Copyright © 2011-2022 走看看