第八章 遞歸(recursion)
8.1 導語
因為一些指導者傾向于先教遞歸作為第一個主要的控制結構,本章會以另一種方式繼續教學,他們是互相獨立的。
在計算機科學中,遞歸是一個最基礎而美妙的概念。如果一個函數調用自身的話,我們就成這個函數是遞歸的。遞歸控制結構是本章的主要話題,但是我們也會在進階話題中了解遞歸數據結構。想要分辨很多問題的遞歸本質,你需要多多的開發練習,直到你掌握為止。你可能會驚訝的發現三四行遞歸程序就能做到的事。
我們會用三種技術互相結合來體現什么是遞歸:龍的故事。程序追蹤和遞歸模板。龍的故事部分是最有爭議的,學生們細化并且認為他很有用,但是計算機專家學者們則很不感冒。如果你不喜歡龍的故事沒那么可以跳過相關小節、其間的部分也會很好的表達意思,只是少了很多生趣。
8.2馬丁和龍
在很久之前的遠古時代,在計算機被發明之前,煉金術師很學到了關于數字的神秘屬性。沒有計算機,他們只能借助龍的力量來為他們工作。龍是具有非凡智慧的生物,但是也很懶惰,脾氣也很壞。最糟糕的一點是有時候會用致命熱度的吐息把他的飼主烤的外焦里嫩,但大部分龍僅僅是不想合作而已,沒有那么暴力。這個故事關于馬丁,一個煉金術師學徒,通過一個超級聰明的懶龍發現遞歸的故事。
有一天師傅給了馬丁一個數字的列表,讓他去地牢里面問問龍,哪一些數字是奇數,馬丁之前總來沒有去過地牢。他拿上了蠟燭,在地牢歐洲的最最深處的陰暗角落,發現了一只老龍,看上去并不是很友好的樣子。馬丁一步步的緩緩前進,他很害怕,他可不想被烤的外焦里嫩。
“你要做什么?”龍疑心重重地看著馬丁,帶著慍怒嘀咕。
“請幫助我,龍,我有一個數字的列表,需要知道哪些數字是奇數?!瘪R丁開始請求“請看”,他在布滿灰塵的地上,用手指寫下了師父給的數字列表。
(3142 5798 6550 8914)
那條龍那天心情不是很好,其實作為一條龍,哪天心情都不好?!皩Σ黄鹆恕饼堈f“我能做的頂多就是告訴你第一個數字是不是奇數,其他的太麻煩了,別來打擾我”。
“但是我必須要知道列表里的所有數字是不是奇數呀,不只是第一個數字”馬丁解釋道。
“那真的是對不起了”龍說“我只看列表的第一個數字,但是你把數字一個個給我看的話或許就能如愿以償了?!?br>
馬丁想了一想,現在也只好聽龍的指示來做了“那第一個數字是不是奇數呀?”他問。
“第一個數字不是奇數”,龍回答。
馬丁用手遮住了列表的第一個元素,然后添上一個左括號。
(5798 6550 8914)
然后又說:“那這個列表呢?”
“第一個元素不是奇數”龍回答說。馬丁然后又遮住更多元素“那現在這個呢?”
(6550 8914)
“第一個元素也不是奇數”龍說,然后環顧了一下四周,看上去很不耐煩,但至少還是比較合作的。
“那還有這個呢?”馬丁追問。
(8914)
“不是奇數”
“那這個呢?”
()
“那是一個空列表!”龍哼了一聲?!八豢赡苁且粋€奇數,因為它里面什么都沒有!”
“非常好”馬丁說,“我現在已經知道了列表里沒有數字是奇數,師傅給我的數字都是偶數?!?br>
“我從沒這樣說過!”龍咆哮著怒吼。馬丁已經問到了火藥味了“我只是告訴你你給我看的每一個列表的第一個數字是不是奇數!”。
“確實如此,龍。那我寫下給你看的所有列表還不好?!?br>
“如果你想的話”,龍很傲嬌的回答。馬丁開始在地上寫:
(3142 5798 6550 8914)
(5798 6550 8914)
(6550 8914)
(8914)
()
“你現在看到了嗎?”馬丁問“通過告訴我每一個列表的第一個元素不是奇數,你已經告訴我原來的列列表沒有奇數了?!?br> “這是個鬼把戲”,龍暴躁的說,“看上去你已經發現了遞歸了,但是你別問我遞歸是什么意思,你必須自己去找到他的意義。”龍閉上眼睛一言不發的休息去了。
8.3 一個搜索奇數的函數
這里有一個遞歸函數anyoddp,如果有任何元素師奇數,就返回T。列表中沒有奇數的話就返回nil。
如果列表是空的,那么anyoddp就會返回nil,正如龍所說,一個不包含任何東西的類表不會是一個奇數。如果一個列表不為空,我們會進入第二個cond語句,然后測試第一個元素。加入第一個元素師奇數,那么就沒有必要再看下去了,可以停止并且返回T了。當地一個元素師偶數,anyoddp就會調用自身來繼續處理列表其余的部分。這就是定義的遞歸部分。
為了根號的理解anyoddp是如何工作的,我們可以使用dtrace來顯示每一個函數調用和每一個返回值。這個dtrace工具在第七章介紹過了,如果你的lisp沒有dtrace的話,可以使用trace代替。
我們一開始會使用一個最簡單的列表:一個空列表,還有一個奇數的列表。
現在請你考慮一個列表中出現偶數的情況,在第一個兩個cond語句哪里會是一個false,所以函數會由第三個語句來結束,他會遞歸調用anyoddp自身來處理列表的剩下的元素。如果rest是nil的話,那么第一個語句就解決問題了,返回nil。
如果這個列表包含兩個元素,一個偶數和一個奇數,遞歸調用將會除法第二個cond語句而不是第一個。
最后,我們來考慮一般情況下,奇數和偶數并存的情況。
請注意在上述函數的例子中沒有不得不遞歸到nil的情況,既然列表(7 8 9)的first是奇數,那么anyoddp就可以停止在那個點上并且返回T。
8.4 馬丁的再訪
“你好!龍先生!”馬丁從快散架的地牢樓梯上走下來的時候說。
“恩???又是你!我已經中了你的遞歸圈套了!”龍嫌棄地看著他。
“我想找到5的階乘是什么?!瘪R丁說“首先,到底階乘是什么意思呀?”
龍正在氣頭上,“我是不會告訴你的,自己去看書吧”。
“好吧”馬丁說“只要告訴我5的階乘是什么,我馬上就走!”。
“”你連階乘是什么意思都不知道來來問我5的階乘是什么?好吧混蛋,我告訴你,但是沒那么容易。5的階乘就是4的階乘的5倍,走的時候把門帶上,不謝。“
“但是4的階乘是什么呢?”,馬丁問,看上去對龍的含糊其辭很是不滿意。
“4的階乘?那當然是3的階乘的4倍了”
“看樣子你要準備告訴我3的階乘是2的3倍了”馬丁說。
“你那么聰明別來找我呀”龍說“現在滾出去!”。
“還沒完呢!”馬丁回答?!?的階乘是1的兩倍,那1的階乘是0的1倍,是吧?”
“0的階乘是1”龍說“這個是你真的要記住的階乘?!?。
“姆。。。這個階乘函數有一個模式,也許我應該將步驟寫下來?!?/p>
“好的”龍說“你可以將所有階乘都遞歸為0的階乘,也就是1 。 現在你可以嘗試在一步步退回。。?!饼堅捳f到一半停住了,他不想表現的樂于助人的樣子。
馬丁又開始寫:
“hey!”馬丁尖叫道,“5的階乘是120,就是答案了,謝謝!”。
“我沒有告訴你答案!”龍暴怒,“我只是告訴你0的階乘是1,n的階乘解釋n倍的n-1的階乘而已,你給自己的應用了rest,遞歸的?!?。
“確實如此”馬丁說“現在我明白遞歸的真正含義了。”。
8.5 一個lisp版本的factorial函數
龍的話給了我們一個階乘函數factorial的定義:n的階乘就是n倍的n-1的階乘0的階乘是1 。函數fact可以遞歸的計算階乘:
下面是lisp如何解決馬丁的問題的:
8.6 龍的夢
這一次馬丁來到地牢里的時候,他發現龍在拼命眨眼,就好像剛剛從一個長夢中蘇醒一樣。
“我做了一個有趣的夢。”龍說,“實際上這是一個遞歸的夢,你想不想聽聽?”
馬丁被龍有好的態度嚇了一跳,網結論煉金術師師傅給的最新的問題,說“請說吧,說說你的夢”。
“很好”,龍開始說了,“昨天晚上我正在盯著一條很長的面包,想著他能夠被切成多少片。于是我真的拿起了刀開始一片片的切起來。我有了一片,面包開始短了,但是始終沒有答案,帶著問題我漸漸入眠。”。
“這就是你夢的內容?”馬丁說。
"是的,有趣的是,我夢到的是另一條龍在做夢切面包,就像我一樣,除了面包變短了。我太想知道到底有多少片了,。但是同樣的問題出現,他像我一樣切面包,像我一樣入睡。"
“所以你沒有找到答案,”馬丁失望地說,“你不知道面包有多長,你也不知道他的有多長,除了他的面包比你的短。”
“是的,但是這還沒完?!饼堈f,“當龍在我的夢里面入睡,也開始做夢。他夢到一頭龍正在切面包。而且這條龍想要知道這條面包有多少片,嘗試這一片片切,但是也沒有得到答案就睡著了。”
“夢中夢!”馬丁尖叫道,“你讓我的大腦一團漿糊是不是最后一條龍也是同樣的一個夢?”
“是的而且他不是最后的那條龍。沒一條龍都夢到一條龍,做著同樣的事情,我正在為這個夢堆砌起一個棧?!?br>
“那你怎么叫醒他們?”馬丁問:
“好的”龍說,“事實上有一條龍到最后的面包變得短到沒有了。你可以稱它是一個空面包。龍就看到那個面包沒有一片存在,所以知道答案是0,于是就沒有入睡?!?br>
“當龍知道前一頭龍的面包沒有的時候,那就知道了自己的面包師1片,也就醒了過來,所以當答案揭曉,龍就會醒來。”。
“還有,當龍夢到的那頭龍醒來的時候,他也就知道自己的面包師兩片既然知道是比他夢到的那頭龍多一片,那也就自然醒過來了。”
“我明白了”馬丁說,“他他是在自己夢到的龍的面包片數加上1,就得到了自己問題的答案,當你醒過來的時候,你的答案是多少?”
“27”龍說,“那是一個很長的夢。”。
8.7 數面包片數的遞歸函數
如果我們將一片面包展現為一個符號,一條面包就是一個符號的列表。納悶問題就是一條面包有多少片就是一個列表由多少個元素的問題。很明顯是LENGTH函數的工作,但是如果我們沒有length的話我們任然可以遞歸地來數。
如果輸入是空列表,那么長度就是0,所以count-slices會簡單的返回0。
如果輸入是列表(X),count-slices就會遞歸地調用自己,來一步步處理輸入,然后在自己的額結果上加上1.
當輸入是一個很長的列表,count-slices不得不進行更深的遞歸來得到空列表返回0,之后的每一個遞歸調用都是在街上加上1 。
8.8 遞歸的三個規則
龍,對馬丁問題的討厭其實是虛偽的矯飾,事實上還是很喜歡教他遞歸的。一天它決定去正式的解釋一下遞歸的含義。龍告訴馬丁要想旅行一樣去著手解決每一個遞歸問題。如果遵從下面三個規則來解決遞歸問題,就總是能夠成功完成旅程。龍這樣解釋著規則:
1.知道什么時候停止
2.決定如何進行下一步
3.將一個旅程破拆成和一個更加小的旅程的合體
我們來看看每一個規則被應用在我們寫的lisp函數的情況,第一條,“知道什么時候停止”,他警告我們任何遞歸函數在進一步遞歸之前都必須檢查這個旅程是不是結束了。一般來說這是第一個cond語句的工作。在anyoddp中,第一個語句就是檢查輸入是不是空列表,如果是函數就停止并返回nil。這個而結成函數fact,當輸入檢測到0的時候就停止。0的遞歸是1,還有正如龍所說,這就是要記住的唯一一個階乘。剩下的都可以遞歸計算出來。在count-slices函數中,第一個cond語句檢查是不是nil,”空面包“,如果輸入是nil那么count-slices就會返回nil。再說一次,這是基于空面包沒有元素,為0的現實基礎上,所以就可以不在遞歸。
第二條規則,”決定如何進行下一步“,要求我們知道將問題破拆成小部分之后如何解決。在anyoddp中就是判斷是橫下的列表的first是不是一個奇數。如果如此我們就返回T。在階乘函數中我們是用一個簡單乘法,將n和n-1的階乘相乘。在count-slices中這一步是+函數。每一片都是從面包上球下來的,我們在加上去就知道了面包的長度。
第三條,”將旅程破拆成一小步加上更小的旅程“,意味著要找到一個方法來遞歸調用自身然后變成更加小的一部分。anyoddp函數就是調用自身來處理獵豹的rest部分,一個比原列表更短的列表,來看是不是有任何奇數。階乘函數遞歸計算n-1的階乘,一個比n的階乘更加簡單的問題,最終我們將通過它來計算出n的階乘。在count-slices中我們遞歸調用一個面包的rest,然后在一個個加上就好。
龍的三個遞歸函數 | ||||
---|---|---|---|---|
函數 | 停止時的輸入 | 返回值 | 進一步處理 | 問題的rest |
ANYODDP | NIL | NIL | (ODDP (FIRST X)) | (ANYODDP (REST X)) |
FACT | 0 | 1 | N ′ ... | (FACT (- N 1)) |
COUNT-SLICES | NIL | 0 | 1 + ... | (COUNT-SLICES (REST LOAF)) |
8.9 馬丁發現無盡遞歸
這一次馬丁到地牢里的時候,帶給了龍一個羊皮卷。“看那,龍"他說“肯定還有其他人是知道遞歸的,我在練技術師的圖書館里找到了這個?!?。
龍猜疑地看著馬丁展開羊皮卷,將燭臺放在尾部來保持平整?!斑@個羊皮卷沒有任何意義”,龍說,“另外,太寫的括號太多了?!?。
"是寫的有些奇怪"馬丁同意,“但是我覺得我發現了一個信息,這是一個計算斐波那契數列的算法?!?br>
"我已經知道如何計算斐波那契數列了"龍說。
“哦?是嘛?怎么計算?”
"為什么告訴你?哼!"龍回答。
"我也知道你不會的",馬丁反擊說,"但是這個羊皮卷告訴我了斐波那契的第n個數字就是n-1個和n-2的和,這是一個遞歸定義,我已經知道如何計算數列了。’’
“羊皮卷還說了什么?”,龍問。
“沒有其他的了,還應該說什么?”
突然龍擺出了一副諂媚的腔調,馬丁發現有些異樣,“親愛的孩子,你可不笨一幫我這個又窮又老的龍一個小小的忙?為我計算一個斐波那契數,一個很小的數就好?!薄?br>
“額。。。我正要上樓清掃魔藥鍋的說”馬丁開始說,但是看到龍臉上悲傷地表情的時候,又說“如果是一個很小的數字,那還行吧”。
“你不會后悔的?!饼堅手Z,“告訴我,第四個斐波那契數是什么?”。
馬丁開始遵循斐波那契算法,那地上寫:
Fib(n) = Fib(n-1) + Fib(n-2)
然后開始計算第四個斐波那契數;
Fib(4) = Fib(3) + Fib(2)
Fib(3) = Fib(2) + Fib(1)
Fib(2) = Fib(1) + Fib(0)
Fib(1) = Fib(0) + Fib(-1)
Fib(0) = Fib(-1) + Fib(-2)
Fib(-1) = Fib(-2) + Fib(-3)
Fib(-2) = Fib(-3) + Fib(-4)
Fib(-3) = Fib(-4) + Fib(-5)
“算完了沒?”,龍賣萌地問。
“還沒有”,出了點小問題,“有些東西出錯了,數字開始想負數無限增長了?!薄?br>
"好的,你快完成了沒?"。
“他看上去是無窮無盡的”,馬丁說,“這個遞歸將永遠進行下去?!?。
"哈哈哈,你看到了吧。你已經陷入了一個無限遞歸了!"。龍幸災樂禍的看,“我早就看到了。”。
"那你怎么不跟我說一聲?"馬丁質問說。
龍做了一個鬼臉,然后哼了一聲,藍色的火焰在鼻孔里冒了一下。“你是不是要永遠依靠龍來幫助你思考?一直讓龍來解決遞歸?”。
馬丁沒有害怕,但是他退了幾步讓煙火散去一些,“好吧,那你是怎么這么快發現問題的,龍?”。
“基礎!孩子,這個羊皮紙告訴你如何進行計算步驟,和如何將大的過程分解成小的,但是卻沒有提到什么時候停止,是因為這個?!?,龍露出牙齒笑起來。
8.10 lisp中的無限遞歸
lisp函數能夠可以無視龍的第一條規則來制造一些無限遞歸函數,我們知道計算這是不會停止的。這里是馬丁的算法的lisp實現。
一般來說,一個好的程序員可以看一下函數就辨別出這個函數是不是無盡遞歸的,但是在一些情況下可能是很難決定的一件事。嘗試追蹤函數C,輸入就用小的正數。
嘗試使用其他1到10的數字來調用C。請注意在輸入數字的大小和遞歸的規模上沒有明顯的額關系。數學家相信,任何正數的輸入都會返回T,也就是說沒有輸入會引起無限遞歸。就是知名的克拉茲猜想。至今還沒有解決的問題,所以我們也不能保證任何輸入都會有T返回。
8.11 遞歸模板
大部分Lisp遞歸函數都符合一些模板。這些函數就是被遞歸函數模板所描述的,這些模板可以用一個填補空格形式來抓住函數的本質。你可以通過選擇一個模板,并且在中間填補空格來創造一個函數,當然也是的,一旦你掌握了這個方法,你就可以使用模板分析現有的函數,只打他們是使用哪一種模式。
8.11.1 雙測試尾遞歸(double-test tail recursion)
我們要學習的第一個模板是雙測試尾遞歸模板,如圖8-1所示?!半p測試”的意思是遞歸函數有兩個測試結束部分,如果其中一個是真的話,那么對應部分的值就會被返回,不會再繼續進行遞歸了。當兩個端測試都是false,那么就結束在最后一個cond語句,這個函數接受輸入,然后進行遞歸調用。這個模板被稱為尾遞歸是因為他的最后一個cond語句出克遞歸之外不做其他的事情。無論遞歸產生什么結果,anyoddp是一個尾遞歸的例子。
8.11.2 單測試尾遞歸(single-test tail recursion)
單測試尾遞歸的是一個更簡單的模板,但是使用沒有那么頻繁。假設我們想要找到一個列表的第一個元字符,可能這個元字符是隱藏在很深的嵌套結構中,我們可以一直使用first,知道找到那個元字符為止。這既是find-first-atom函數的作用。
一般來說,單測試遞歸的使用情況是在我們知道函數總是可以找到元素的。find-first-atom函數因為不斷地使用first,所以肯定可以找到第一個元字符。在anyoddp函數中,例如,第二個測試檢查是不是找到了一個奇數,但是第一個測試是檢查列表是不是已經到了最后空的狀態,那時候就應該返回nil.
8.11.3 增強型遞歸
增強型遞歸函數,像count-slices會一位一位的建立他們的結果。我們稱這個過程是增強。不是吧一個問題分解成一個廚師步驟然后增加到更小一點的問題上,與這個策略不同,他們的策略就是分割成一個更小的部分然后加成一個最終結果。最終的一部是由一個增強型的值和應用到之前遞歸調用的結果來組成的。在count-slices中,例如,我們首先做一個遞歸調用然后給結果加上1來得到結果。
在尾遞歸函數中,結果沒有任何增強也是可以的。因此,尾遞歸返回的值總是等于函數定義中的端值之一。他并不是一位一位的建立結果的。相比總是返回T或者nil的anyoddp,他從不增強他的結果。
8.12 基本模板中的變量
到現在為止我們學習的模板是有很多用處的,使用它們的具體方式在lisp編程中特別的普遍或者貼別值得注意。在本節中我們會講解基本模板中的變量。
8.12.1 列表組合遞歸
在lisp中使用最頻繁的就是列表組合遞歸。增強遞歸的特殊情況就在增強函數是cons。我們創造一個新的內存單元,作為每一個遞歸調用的返回。因此,遞歸的深度就等于結果中內存單元鏈條的長度,在加上1.(因為最后一個調用返回nil,而不是一個內存單元)。
8.12.2 多個變量的同時遞歸
多個變量的同時遞歸是一個任何遞歸模板的直接擴展。不是只有一個輸入,函數有多個,一個或多個輸入會被遞歸調用。例如,假設我們想要寫一個nth函數的遞歸版本,叫做mynth,?;仡櫼幌?,(NTH 0 x)就是(FIRST x),這告訴我們哪一個端測試會被使用。隨著每一個遞歸調用,我們接受列表x的后續rest一步步進行處理。結果函數顯示了單測試尾遞歸是在兩個變量的同時遞歸。這個模板在圖8-5中有顯示。下面是追蹤兩個變量同時被處理的過程。
8.12.3 條件增強型
在一些列表處理問題中,我們想要跳過某些特定元素,只是用其中一部分來計算結果。這就是條件增強型。例如,在extract-symbols中,只有那么是字符的元素才會被包含在結果中。
在extract-symbols的函數體中,包括了兩個遞歸調用。一個調用時嵌套在一個增強表達式中的,這樣是講一個新的元素組合進現有的結果中。另一個調用是非增強的,結果只是簡單返回了。在后續的追蹤輸出中你會注意到有些時候兩個相繼的調用時飯回一個值,這是因為每一對調用都選擇了那個非增強的語句,當增強語句被選擇,結果頁不在相同。
8.12.4 多遞歸
如果一個函數在每一個調用中制造了超過一個遞歸調用的話,函數就是多遞歸的。(不要和多個同時遞歸混淆了,之前的只是將多個變量進行同時處理,并不在每一個調用中使用多個遞歸。)斐波那契函數是一個景點的多遞歸例子,fib(n)調用自身兩次,依次是fib(n-1),另一次是fib(n-2)。兩次調用的結果使用+來結合。
將多遞歸的過程具象化的一個好方法就是看追蹤輸出的嵌套調用的形狀。我們來定義,終極調用的意思就是不再往更深的地方遞歸了,在先前所有的函數中,連續調用嚴格來說是一個個嵌套在里面的,內部的最終調用也是一個終極調用。然后,從內到外的直接返回值。但是由于多遞歸函數FIB的使用,每一個調用都會產生兩個新的調用,這兩個是內嵌在現有的調用中的,但是他們不能互相嵌套。并不是一步步出現在上層調用中,多遞歸函數因此有很多終極調用。在接下來的追蹤輸出中,有三個終極調用,兩個非終極調用。
8.13 樹和car/cdr遞歸
有時候我們想要來處理一個嵌套列表中的所有元素,而不僅僅是頂層元素。如果列表時不規則形狀的,比如(((GOLDILOCKS . AND)) (THE . 3) BEARS),這樣可能會更加困難了。當我們來寫自己的函數,我們不知道輸入會有多長或者嵌套會有多復雜
解決這個問題的技巧并不是去考慮這個函數的輸入是不規則的嵌套列表,而是考慮一顆二進制樹的結構(請見下面的插圖)二進制數是很規則的,每一個節點不是元字符就是兩個分支的組合,car和cdr。因此我們的函數所做的就是處理元字符,遞歸調用自身來處理每一個節點的car和cdr。這個技術叫做car/cdr遞歸;這是多遞歸的特殊情況。
舉個例子,假設我們想要一個函數find-number來搜索一個樹,并返回出現的第一個數字,沒有的話就返回nil。然后我們應該使用numberp和atom作為我們的終端測試,還有or來做組合器。請注意or是一個條件,只要有一個or語句被求值為true,or就會停止然后返回那個值。因此我們不是一定要搜索整個樹;函數有可能停止遞歸,只要第一個非nil的值出現。
除了樹搜索,另一個car/cdr遞歸的廣泛使用的地方時通過使用cons構造器來建造樹。例如,有一個函數接受樹作為輸入然后返回一個新的樹,結果中所有的非nil元字符都被替換成字符Q。
8.14 使用輔助函數
對于一些問題來說,構造解決方案是一個遞歸函數加上一個輔助函數式很有用的。遞歸函數做大部分工作。副主函數是你從頂層的調用之一。他提供的是一些在遞歸開始或者結束后的特殊服務。例如,加入我們想要寫一個函數count-up來數數,從1到n。
這個問題比count-down要難一些,因為內部的遞歸次奧用必須是輸入到達5的時候結束遞歸(而不是0)。一般來說,最簡單的方法就是去支持遞歸函數的原始值所以他能決定什么時候停止。我們必須支持一個附加的參數。那就是一個計數器,來告訴我們函數的遞歸由多深了。副主函數的工作就是提供計數器的原始值。
8.15 藝術和文學中的遞歸
遞歸不止在計算機編程中出現,也會在文學故事和畫作中有出現。經典的一千零一夜故事中就包括故事中的故事中的故事,他給出了一種遞歸的趣味性。在視覺上的相似表達出現在蘇斯鄙視的注明畫作《戴高帽的貓》里。貓的帽子里是另一只戴著帽子的貓,就如此遞歸。
一些最有想象力的遞歸展現和自我引用的表達是荷蘭藝術家M.C.Escher的版畫《畫手》。
小結
遞歸是一個非常強大的控制結構,也是計算機科學中最重要的概念。如果一個函數會調用自身,那么他就被成為是遞歸的。為了寫一個遞歸函數,我們必須解決龍提出的三個規則引出的問題。
- 知道什么時候停止
- 決定如何進行下一步
- 將整個過程拆分成為更小的過程加上簡單步驟
我們在本章學習了很多遞歸模板,遞歸模板觸及的是常規遞歸問題的本質。他們會在函數定義的時候被使用,或者在分析現有函數的時候被使用。下面是現在為止學到的遞歸模板:
- 雙測試尾遞歸Double-test tail recursion.
- 單測試尾遞歸Single-test tail recursion.
- 單測試增強型Single-test augmenting recursion.
- 列表組合型List-consing recursion.
- 多變量的同時遞歸Simultaneous recursion on several variables.
- 條件增強型Conditional augmentation.
- 多遞歸調用Multiple recursive calls.
- cad/cdr遞歸CAR/CDR recursion.
Lisp Toolkit: The Debugger
所有的Lisp初學者都會學習一個debugger命令,因為只要輸入錯誤了,就會進入debugger,就不得不使用命令出來!lisp實現在debugger方面本質上差別很大,所以從錯誤中返回也沒有一個統一的標準。一些人肯呢過已經嘗試輸入q或者quit或者:a來嘗試abort。其他人可能使用ctrl-c或者ctrl-g組合鍵。在任何情況下,你都可以退出來,但是為什么不對呆一會兒呢?
debugger并不是真的吧你程序里的bug給移除了。他所做的是讓你在錯誤發生的時候檢查計算過程的狀態。這也使得它成為一個學習遞歸的好工具。我們可以使用break函數來進入debugger,所處的是計算過程的一個斷點。break的參數是一個消息,被字符串引用,當進入debugger的時候就會打印出來,下面是一個fact的修改后版本來顯示break的使用。
現在我們就在debugger中了,“Debug>”就是提示符。(你的debugger也許是用不同的提示符)。我們能做的其中一件事情就是展現控制棧的回溯,會展現所有的現在棧中的遞歸調用。如果你對“控制?!焙汀皸钡刃g語不熟悉的話,只要在debugger中慢慢摸索就可以漸漸掌握訣竅了。(控制棧是lisp追蹤嵌套函數調用的方式,一個棧幀就是描述其中一個函數調用的入口),在我們的debugger中,展現回溯的命令就是bk。
bk命令的變量允許不同種類的控制棧信息展現。在我的debugger中,bkfv就會給出一個函數名字和本地變量的表示。
在debugger內部的時候,我們可以看到變量的值,并且可以使用任意的lisp表達式來使用它們。
當我們進入debugger,我們正在棧的頂層。我們可以使用命令進入棧內,往上或者往下移動。如果我們向下移動,我們會看到其他叫做n的本地變量。
最后,我們使用debugger來從任何棧中的調用返回。這導致的計算會好像函數正常返回了一樣。
當我們從當前棧幀返回10的時候,計算過程假定在那個點,產生的值是5×4×3×10 = 600
你的debugger也許和我的看上去并不是完全相同,也許提供某些不一樣的功能,但是檢查控制棧這個基本思想在所有lisp中是一樣的。你可以看看你的lisp用戶手冊來看看自己的實現支持那些命令。鍵入help或者:h或者?來看看你的debugger有那些命令。
第八章進階話題
8.16 尾遞歸的優點
請記住尾遞歸函數在遞歸調用之后是沒有任何動作的,遞歸函數返回什么,他就返回什么。anyoddp就是一個為遞歸函數,但是count-slices就不是。如果我們追蹤count-slices來看,我們會看到每一個調用都會產生一個不同的返回值(由于增強的原因)。在一個尾遞歸函數中,所有的調用在終極調用中都是返回同一個值。
一般來說,只要可能的話,寫遞歸函數最好就是用尾遞歸,因為lisp系統可以比普通遞歸函數更有效率地執行尾遞歸函數。他們用一個跳轉來代替遞歸調用。很多lisp編譯器都是自動優化的,一些解釋器也是。
創造一個普通函數的尾遞歸版本最普遍的做法就是引入一個附加變量來積累增強的值。例如,下面一個例子:
在追蹤TR-COUNT-SLICES的時候,你會注意到n的值是隨著每一個調用而增長的。終極調用計算出返回值,4;這個值會不加更改的傳回去。
另一個增強型如何被消除的方法就是引入一個附加變量,這個例子就是reverse函數。為了反轉一個長度為n的列表,我們可以遞歸反轉列表的剩余部分,然后確定第一個元素。
但是這個定義并不是尾遞歸,在遞歸調用返回后,結果被append增強,reverse的雙輸入,尾遞歸定義,就是使用一個附加的變量來建立結果。
并不是所有的函數都有尾遞歸版本,任何多遞歸函數,比如fib,就不能夠簡單轉換為尾遞歸,在第一個遞歸結束后,還有代碼等著去執行。
8.17 寫一個新的函數式操作
我們可以使用funcall來調用一個用戶支持的函數。這允許我們去寫一個我們自己的函數式操作。例如,下面的mapcar的簡單版本就只是支持簡單列表。
我們的所提供的函數my-mapcar必須是一個單輸入函數,多少輸入都會被funcall調用。
8.18 特殊函數labels
到現在為止我們已經寫過像獨立的defun這樣的輔助函數。這有點單薄了,既然輔助函數是定義在頂層,那么就又能被其他函數誤調用。第二,更加嚴重的問題是輔助函數使用defunct定義的,導致他看不到任何主函數的本地變量。這些問題都將被label解決。
特殊函數labels允許我們在主函數內部定義一個本地函數,就像樂天能讓我們建立本地變量一樣。這兩個語法格式是相似的。
這個函數體可以調用任何本地變量。本地函數可以互相調用,也可以指向上層的變量。
In the following example, notice that COUNT-UP-RECURSIVELY
references N, the input to COUNT-UP
請注意,接下來的例子count-up-recursively指向n,count-up的輸入。
使用labels的一個缺點就是在大部分的lisp實現當中,在內部定義的一個labels表達式是不能被追蹤的函數。但是你仍然可以使用step來步進追蹤手動求值,如果需要的話。
8.19 遞歸數據結構
本章被用來講解用遞歸定義函數,數據結構也有遞歸定義??紤]下接下來的s表達式定義(字符表達式):
S表達式這個屬于是被用在自身定義的內部。這個就是遞歸數據結構,S表達式就是一個應用十分廣泛的遞歸數據結構實例,在計算機科學的所有領域都有十分重要的應用,叫做樹。還有另一個樹的例子,這一次表現是數學表達式。
樹底部的節點叫做終極節點,因為他們沒有任何分支子節點。剩下的節點被叫做非終極節點,一棵樹可以被遞歸定義:
樹很自然的由列表來表現。列表((3 + 5)-(8 + 6))對應的樹,我們來看看另一個數學表達式:
這個樹表述的事實是這樣,一個非終極節點的分支需要有同樣的長度。表達式((2 + 2) - (3 *(4 * (12 / 6))))的列表表示我們可以遞歸定義如下: