3、函數與遞歸

 


3、函數與遞歸                                  返回目錄頁
html

一、函數的嵌套調用
二、自定義函數
三、輔助函數
四、匿名函數
五、單行函數
六、遞歸


引用一個MMA粉絲一段話:
Mathematica支持不少的編程範式(有多是最多的),其中最爲高效的應該就是函數式了,熟悉一點函數式語言的人再來接觸Mathematica可能會倍感親切。經過純函數(至關於Lambda演算)、高階函數(Nest、Fold、Map、Apply等等)等各類函數式編程的技巧,你能夠輕易寫出簡潔到爆的程序,並且絕大部分狀況下都比過程式版本高效得多。
( 來源於知乎:https://www.zhihu.com/question/20324243

習慣於過程式編程的人,每每很不習慣這種函數式編程。開始的時候,這種反應是正常的。
咱們學習函數式編程,主要是由於能夠開闊本身的視野,給本身一個看問題的不一樣的視角。


-------------------------------------------------------------------------
一、函數的嵌套調用
幾個函數的依次做用,被稱做嵌套函數調用(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]

你可能會以爲,這個洗牌程序洗出來的牌,太有規律了,不夠亂。不急,這副牌會跟隨咱們很長時間,之後還會不斷玩牌。

從這一節,咱們看到,函數式編程的第一大特色:啥都是函數,函數套函數,層層疊疊。
不過,咱們已經掌握了幾種解讀嵌套函數的技巧。


-------------------------------------------------------------------------
二、自定義函數
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]
那就是六我的在玩德州梭哈,每一個人發兩張牌。而後,在中間發五張牌。

發現木有?咱們在這章到如今使用的內置函數,所有都是在前一章表操做函數中學過的。


-------------------------------------------------------------------------
三、輔助函數
輔助(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格式局部化,常常是個好主意。
局部化名稱的顏色,設置得顯眼一點,也是個好主意。在菜單  編輯/偏好設置  中設置。


-------------------------------------------------------------------------
四、匿名函數
匿名函數(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


-------------------------------------------------------------------------
五、單行函數
單行函數,能夠理解爲無名函數的一個運用。
一個自定義函數一個語句解決。

關於約瑟夫問題,是指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相關的數據。。*)


-------------------------------------------------------------------------
六、遞歸
遞歸是這樣一種函數:在自定義函數時,函數體內用到函數名自己。
遞歸在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

從而咱們看到了二叉樹的不一樣表達形式。數據結構

 

 

++++++++++++++++++++++++++++++++++++++++++dom

擴展閱讀:《計算機程序的構造和解釋(SICP)》,傳說中的MIT教程,函數式編程必讀書。
豆瓣上的評價(連接)
這本書的中文版翻譯得很好(做者叫裘宗燕,聽起來是個女士,其實是個大男人:))。這本書中,用的是Scheme語言,不是僞代碼。下載一個PLT Scheme,就能夠玩Lisp方言Scheme了。(這本書的中文版的pdf,及玩PLT Scheme的安裝程序,均在目錄頁的百度雲中提供下載。)
知乎上的討論(連接)







                    Top










編輯器

相關文章
相關標籤/搜索