Haskell遞歸

maximum 函數取一組可排序的 List(屬於 Ord Typeclass) 作參數,並回傳其中的最大值。想一想,在命令式風格中這一函數該怎麼實現。極可能你會設一個變量來存儲當前的最大值,而後用循環遍歷該 List,若存在比這個值更大的元素,則修改變量爲這一元素的值。到最後,變量的值就是運算結果。唔!描述如此簡單的算法還頗費了點口舌呢!html

如今看看遞歸的思路是如何:咱們先定下一個邊界條件,即處理單個元素的 List 時,回傳該元素。若是該 List 的頭部大於尾部的最大值,咱們就能夠假定較長的 List 的最大值就是它的頭部。而尾部若存在比它更大的元素,它就是尾部的最大值。就這麼簡單!如今,咱們在 Haskell 中實現它算法

maximum' :: (Ord a) => [a] -> a   
maximum' [] = error "maximum of empty list"   
maximum' [x] = x   
maximum' (x:xs)    
    | x > maxTail = x   
    | otherwise = maxTail   
    where maxTail = maximum' xs

如你所見,模式匹配與遞歸簡直就是天造地設!大多數命令式語言中都沒有模式匹配,因而你就得造一堆 if-else 來測試邊界條件。而在這裏,咱們僅須要使用模式將其表示出來。第一個模式說,若是該 List 爲空,崩潰!就該這樣,一個空 List 的最大值能是啥?我不知道。第二個模式也表示一個邊緣條件,它說, 若是這個 List 僅包含單個元素,就回傳該元素的值。編程

改用 max 函數會使代碼更加清晰。若是你還記得,max 函數取兩個值作參數並回傳其中較大的值。以下即是用 max 函數重寫的 maximun'數據結構

maximum' :: (Ord a) => [a] -> a   
maximum' [] = error "maximum of empty list"   
maximum' [x] = x   
maximum' (x:xs) = max x (maximum' xs)

太漂亮了!一個 List 的最大值就是它的首個元素與它尾部中最大值相比較所得的結果,簡明扼要。函數式編程

如今咱們已經瞭解了遞歸的思路,接下來就使用遞歸來實現幾個函數. 先實現下 replicate 函數, 它取一個 Int 值和一個元素作參數, 回傳一個包含多個重複元素的 List, 如 replicate 3 5 回傳 [5,5,5]. 考慮一下, 我以爲它的邊界條件應該是負數. 若是要 replicate 重複某元素零次, 那就是空 List. 負數也是一樣, 不靠譜.函數

replicate' :: (Num i, Ord i) => i -> a -> [a]   
replicate' n x   
    | n <= 0    = []   
    | otherwise = x:replicate' (n-1) x

在這裏咱們使用了 guard 而非模式匹配, 是由於這裏作的是布林判斷. 若是 n 小於 0 就回傳一個空 List, 不然, 回傳以 x 做首個元素並後接重複 n-1 次 x 的 List. 最後, (n-1) 的那部分就會令函數抵達邊緣條件.測試

Note: Num 不是 Ord 的子集, 表示數字不必定得拘泥於排序, 這就是在作加減法比較時要將 Num 與 Ord 類型約束區別開來的緣由.ui

接下來實現 take 函數, 它能夠從一個 List 取出必定數量的元素. 如 take 3 [5,4,3,2,1], 得 [5,4,3]. 若要取零或負數個的話就會獲得一個空 List. 一樣, 如果從一個空 List中取值, 它會獲得一個空 List. 注意, 這兒有兩個邊界條件, 寫出來:spa

take' :: (Num i, Ord i) => i -> [a] -> [a]   
take' n _   
    | n <= 0   = []   
take' _ []     = []   
take' n (x:xs) = x : take' (n-1) xs

首個模式辨認若爲 0 或負數, 回傳空 List. 同時注意這裏用了一個 guard 卻沒有指定  部分, 這就表示  若大於 0, 會轉入下一模式. 第二個模式指明瞭若試圖從一個空 List 中取值, 則回傳空 List. 第三個模式將 List 分割爲頭部和尾部, 而後代表從一個 List 中取多個元素等同於令  做頭部後接從尾部取  個元素所得的 List. 假如咱們要從  中取 3 個元素, 試着從紙上寫出它的推導過程
otherwisenxn-1[4,3,2,1]
reverse' :: [a] -> [a]   
reverse' [] = []   
reverse' (x:xs) = reverse' xs ++ [x]


繼續下去!code

Haskell 支持無限 List,因此咱們的遞歸就沒必要添加邊界條件。這樣一來,它能夠對某值計算個沒完, 也能夠產生一個無限的數據結構,如無限 List。而無限 List 的好處就在於咱們能夠在任意位置將它斷開.

repeat 函數取一個元素做參數, 回傳一個僅包含該元素的無限 List. 它的遞歸實現簡單的很, 看:

repeat' :: a -> [a]   
repeat' x = x:repeat' x

zip 取兩個 List 做參數並將其捆在一塊兒。zip [1,2,3] [2,3] 回傳 [(1,2),(2,3)], 它會把較長的 List 從中間斷開, 以匹配較短的 List. 用 zip 處理一個 List 與空 List 又會怎樣? 嗯, 會得一個空 List, 這即是咱們的限制條件, 因爲 zip 取兩個參數, 因此要有兩個邊緣條件

zip' :: [a] -> [b] -> [(a,b)]   
zip' _ [] = []   
zip' [] _ = []   
zip' (x:xs) (y:ys) = (x,y):zip' xs ys

前兩個模式表示兩個 List 中若存在空 List, 則回傳空 List. 第三個模式表示將兩個 List 捆綁的行爲, 即將其頭部配對並後跟捆綁的尾部. 用 zip 處理 [1,2,3] 與 ['a','b'] 的話, 就會在 [3] 與 [] 時觸及邊界條件, 獲得(1,'a'):(2,'b'):[] 的結果,與 [(1,'a'),(2,'b')] 等價.

 

再實現一個標準庫函數 -- elem! 它取一個元素與一個 List 做參數, 並檢測該元素是否包含於此 List. 而邊緣條件就與大多數狀況相同, 空 List. 你們都知道空 List 中不包含任何元素, 便沒必要再作任何判斷

elem' :: (Eq a) => a -> [a] -> Bool   
elem' a [] = False   
elem' a (x:xs)   
    | a == x    = True   
    | otherwise = a `elem'` xs

這很簡單明瞭。若頭部不是該元素, 就檢測尾部, 若爲空 List 就回傳 False.

 

快速"排序

假定咱們有一個可排序的 List, 其中元素的類型爲 Ord Typeclass 的成員. 如今咱們要給它排序! 有個排序算法很是的酷, 就是快速排序 (quick sort), 睿智的排序方法. 儘管它在命令式語言中也不過 10 行, 但在 Haskell 下邊要更短, 更漂亮, 儼然已經成了 Haskell 的招牌了. 嗯, 咱們在這裏也實現一下. 或許會顯得很俗氣, 由於每一個人都用它來展現 Haskell 究竟有多優雅!

它的類型聲明應爲 quicksort :: (Ord a) => [a] -> [a], 沒啥奇怪的. 邊界條件呢? 如料,空 List。排過序的空 List 仍是空 List。接下來即是算法的定義:排過序的 List 就是令全部小於等於頭部的元素在先(它們已經排過了序), 後跟大於頭部的元素(它們一樣已經拍過了序)。 注意定義中有兩次排序,因此就得遞歸兩次!同時也須要注意算法定義的動詞爲"是"什麼而非"作"這個, "作"那個, 再"作"那個...這即是函數式編程之美!如何才能從 List 中取得比頭部小的那些元素呢?List Comprehension。好,動手寫出這個函數!

quicksort :: (Ord a) => [a] -> [a]   
quicksort [] = []   
quicksort (x:xs) =   
  let smallerSorted = quicksort [a | a <- xs, a <= x]  
      biggerSorted = quicksort [a | a <- xs, a > x]   
  in smallerSorted ++ [x] ++ biggerSorted

咱們已經寫了很多遞歸了,也許你已經發覺了其中的固定模式:先定義一個邊界條件,再定義個函數,讓它從一堆元素中取一個並作點事情後,把餘下的元素從新交給這個函數。 這一模式對 List、Tree 等數據結構都是適用的。例如,sum 函數就是一個 List 頭部與其尾部的 sum 的和。一個 List 的積即是該 List 的頭與其尾部的積相乘的積,一個 List 的長度就是 1 與其尾部長度的和. 等等

 

再者就是邊界條件。通常而言,邊界條件就是爲避免進程出錯而設置的保護措施,處理 List 時的邊界條件大部分都是空 List,而處理 Tree 時的邊界條件就是沒有子元素的節點。

處理數字時也與之類似。函數通常都得接受一個值並修改它。早些時候咱們編寫過一個計算 Fibonacci 的函數,它即是某數與它減一的 Fibonacci 數的積。讓它乘以零就不行了, Fibonacci 數又都是非負數,邊界條件即可以定爲 1,即乘法的單比特。 由於任何數乘以 1 的結果仍是這個數。而在 sum 中,加法的單比特就是 0。在快速排序中,邊界條件和單比特都是空 List,由於任一 List 與空 List 相加的結果依然是原 List。

使用遞歸來解決問題時應當先考慮遞歸會在什麼樣的條件下不可用, 而後再找出它的邊界條件和單比特, 考慮參數應該在什麼時候切開(如對 List 使用模式匹配), 以及在何處執行遞歸.

轉自:http://learnyouahaskell-zh-tw.csie.org/zh-cn/recursion.html

相關文章
相關標籤/搜索