遞歸函數(二):編寫遞歸函數的思路和技巧
來自專欄 業餘程序員的個人修養
遞歸,是一個熟悉而陌生的概念,說它熟悉,是因為人們經常提起它,
而說它陌生,指的是人們在實際編程中幾乎不會主動使用它。給定一個問題,如果本質上它能看做一個調用自身的規模較小的一個子問題來求解,
那麼給出一個遞歸的演算法解,就是很自然的。然而,即使是這樣,編製一個遞歸函數也是一件令人頭疼的事情。
本系列文章的目的,可能並不局限於指出如何編寫一個遞歸函數,
而是期望想從遞歸函數開始,了解它相關的科學知識,以達到對不同領域觸類旁通的效果。1. 從一個簡單的例子開始
首先,我們來重溫一下遞歸的概念,
維基百科上是這樣描述的,遞歸(recursion),在數學與計算機科學中,是指在函數的定義中使用函數自身的方法。
我們來看一個簡單的例子吧。(Haskell代碼
fact :: Int -> Intfact 1 = 1fact n = n * fact (n-1)
在這個例子中,
第一行fact :: Int -> Int
表示了fact
函數的類型,第二行和第三行定義了函數fact
,我們看到第三行,在對fact
函數定義的時候,等式右邊又出現了fact
,
這樣定義的函數fact
是遞歸的。
我們調用一下fact
,來看看結果,
fact 103628800
嗯嗯,fact
就是階乘函數。
2. 寫遞歸函數的步驟
那麼,給定一個問題,我們編寫一個遞歸函數,要如何開始呢?
(1)遞推式
首先,我們要找到「遞推式」。
例如,在數學上階乘的定義是, ,這樣的表述形式,不具有遞推性。
我們先要想辦法把 用 表示出來。經過思考之後,我們可以證明, ,
於是,我們就走出了關鍵的第一步,得到了「遞推式」。(2)找出終止條件
有了「遞推式」還不行,我們還需要確定遞推在什麼時候終止。
我們知道 , , ,等等,因此,我們只需要指定 ,那麼遞推就會在 ,當 的時候終止了。終止,就是不再調用規模更小的問題了。
這時,終止條件是 。(3)用數學歸納法證明解的正確性
這一步是很重要的,有很多人都缺少證明遞推式正確性的環節,
但是,考慮到介紹數學歸納法及其擴展會佔用不少篇幅,這裡先略去,下一篇我們再回來討論它。這裡,我們先假定,根據「遞推式」和「終止條件」,使用數學歸納法,
我們已經證明了這樣定義的 就是 。(4)根據遞推式和終止條件,編寫程序
有了「遞推式」和「終止條件」,再編寫程序就水到渠成了。
很多人一上來就開始編碼,就會感覺毫無頭緒。我們再來看下那段程序,
fact :: Int -> Intfact 1 = 1fact n = n * fact (n-1)
這不就是「遞推式」和「終止條件」的忠實表示嗎?
我們用fact 1 = 1
表示了 ,用fact n = n * fact (n-1)
表示了 。
3. 小技巧
我們再看個複雜一點的例子。
在實際項目中,我們可能會遇到循環 次的場景,
在循環過程中,我們會根據索引進行運算,然後將某些符合條件的運算放到最終的結果中。例如,我們選擇10以內的所有偶數,
[x|x <- [0..9], x `mod` 2 == 0][0,2,4,6,8]
使用以上列表解析(list comprehension)的方法,我們可以快速得到結果。
但是這裡,我們想要拿它來舉例,介紹一個編寫遞歸函數常用的小技巧。為了通用性,我們考慮循環n
次,將索引傳入函數fn
,
fn
的返回值,將結果放入一個列表中,完成這個功能的函數我們記為myLoop
。(1)困境
根據前文介紹的編寫步驟,我們需要先找到「遞推式」和「終止條件」。
「終止條件」怎麼寫呢?
假如我們定義的遞歸函數稱為myLoop
,那麼 就是終止條件,它應該返回一個列表。但是這個列表在參數中沒有,它隨著遞歸調用的過程「積累」得到的。
好吧,那我們看「遞推式」。
要用 的結果計算出來,我們需要先用索引調用fn
,然後再根據fn
的返回值,放入結果列表,再繼續調用 。可是,索引從哪來呢?(n
不是索引,因為索引從0開始,而n
是逐漸變小的。這是兩個典型的困難,
其一,我們在遞歸的過程中「積累」了某些東西,其二,我們需要傳遞和遞歸過程相關的「索引」。(2)解法
這時候,我們的小技巧就有用武之地了。
我們可以編寫一個輔助的遞歸函數,通過增加參數的辦法,提高靈活性。
例如,我們可以編寫一個輔助函數myLoop
,然後用myLoop
來實現myLoop
。
myLoop :: Int -> (Int -> Maybe a) -> [a]myLoop n fn = myLoop n 0 fn []myLoop :: Int -> Int -> (Int -> Maybe a) -> [a] -> [a]myLoop 0 i fn lst = lstmyLoop n i fn lst = case fn i of Just x -> myLoop (n-1) (i+1) fn (lst++[x]) Nothing -> myLoop (n-1) (i+1) fn lst
以上,我們為myLoop
增加了參數i
和lst
,分別表示「索引」和「積累」的列表。
myLoop
就可以用myLoop
來實現了。別忘了測試一下最終的結果,
myLoop 10 (x -> if x `mod` 2 == 0 then Just x else Nothing)[0,2,4,6,8]
(3)其他考慮
合理的利用遞歸函數的返回值,會減少附加參數的數量,例如,
myLoop :: Int -> (Int -> Maybe a) -> [a]myLoop n fn = myLoop n 0 fnmyLoop :: Int -> Int -> (Int -> Maybe a) -> [a]myLoop 0 i fn = []myLoop n i fn = case fn i of Just x -> x:(myLoop (n-1) (i+1) fn) Nothing -> myLoop (n-1) (i+1) fn
但最終得到的遞歸函數就不是尾遞歸了,
關於尾遞歸,我們將在後續文章中討論它。參考
維基百科 - 遞歸
維基百科 - 遞推關係式下一篇:遞歸函數(三):歸納原理
推薦閱讀:
※你好,類型(七):Recursive type
※POJ 2694:逆波蘭表達式
※模擬狀態機消除遞歸心得
※循環、迭代與遞歸
TAG:遞歸 |