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"}}}]
總的思路是,把二叉樹圖形,轉化爲表,再用自定義函數,把錶轉化爲縮進格式的文本。
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
編輯器