Haskell递归

maximum 函数取一组可排序的 List(属于 Ord Typeclass)
做参数,并回传其中的最大值。想想,在命令式风格中这一函数该怎么实现。很可能你会设一个变量来存储当前的最大值,然后用循环遍历该
List,若存在比这个值更大的元素,则修改变量为这一元素的值。到最后,变量的值就是运算结果。唔!描述如此简单的算法还颇费了点口舌呢!

现在看看递归的思路是如何:我们先定下一个边界条件,即处理单个元素的 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 类型约束区别开来的原因.

接下来实现 take 函数, 它可以从一个 List 取出一定数量的元素.
如 take 3 [5,4,3,2,1], 得 [5,4,3].
若要取零或负数个的话就会得到一个空 List. 同样, 若是从一个空 List中取值, 它会得到一个空 List. 注意, 这儿有两个边界条件,
写出来:

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 却没有指定 otherwise 部分, 这就表示 n 若大于 0, 会转入下一模式. 第二个模式指明了若试图从一个空 List 中取值, 则回传空 List. 第三个模式将 List 分割为头部和尾部, 然后表明从一个 List 中取多个元素等同于令 x 作头部后接从尾部取 n-1 个元素所得的 List. 假如我们要从 [4,3,2,1] 中取 3 个元素, 试着从纸上写出它的推导过程
reverse‘ :: [a] -> [a]   
reverse‘ [] = []   
reverse‘ (x:xs) = reverse‘ xs ++ [x]

继续下去!

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



时间: 2024-10-10 08:28:43

Haskell递归的相关文章

Python学习笔记八:文件操作(续),文件编码与解码,函数,递归,函数式编程介绍,高阶函数

文件操作(续) 获得文件句柄位置,f.tell(),从0开始,按字符数计数 f.read(5),读取5个字符 返回文件句柄到某位置,f.seek(0) 文件在编辑过程中改变编码,f.detech() 获取文件编码,f.encoding() 获取文件在内存中的编号,f.fileno() 获取文件终端类型(tty.打印机等),f.isatty() 获取文件名,f.name() 判断文件句柄是否可移动(tty等不可移动),f.seekable() 判断文件是否可读,f.readable() 判断文件是

PromiseKit 解析 (二) 递归 IOS

在PromiseKit中,里面涉及了很多的递归操作,而递归操作,对于大部分人理解起来都是比较麻烦的(包括我),而github中一些高上大的第三方库,无一不涉及了很漂亮的递归操作,因此理解递归操作是很必要的.如果想成为尽可能优秀的程序猿. 递归操作:自己调用自己,不停的压栈,最后出栈.(说的简单,但是还是理解起来还是比较复杂的) 我们来接着看看PromiseKit的递归操作,看看它的链式结构式怎么样实现的. 相信在上一章就可以了解到,then操作最后返回的是一个新的PMKPromise,然后返回的

Haskell高阶函数

Haskell functions can take functions as parameters and return functions as return values. A function that does either of those is called a higher order function. Higher order functions aren't just a part of the Haskell experience, they pretty much ar

Haskell函数的语法

本章讲的就是 Haskell 那套独特的语法结构,先从模式匹配开始.模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据. 在定义函数时,你可以为不同的模式分别定义函数本身,这就让代码更加简洁易读.你可以匹配一切数据类型 --- 数字,字符,List,元组,等等.我们弄个简单函数,让它检查我们传给它的数字是不是 7. lucky :: (Integral a) => a -> String   lucky 7 = "LUCKY NUMBER SEVEN!" 

逆转序列的递归/尾递归(+destructuring assignment)实现(JavaScript + ES6)

这里是用 JavaScript 做的逆转序列(数组/字符串)的递归/尾递归实现.另外还尝鲜用了一下 ES6 的destructuring assignment + spread operator 做了一个更 functional 的版本(只支持数组). 正确性能通过测试(参见 放在我 Github 上的 demo,顺手写了一个小小的测试框架),不过效率就要打问号了——特别是用了 ES6 特性的版本.这里主要是写来玩 JS 的函数式特性的. 1. 逆转序列的递归实现 先用 Haskell 实现做草

单向链表反转算法——递归版和迭代版

最近在做笔试题时,遇到一道编程题:单向链表反转算法.一时紧张,没写出来就提前交卷了,然而交完卷就想出来了... 最初想出来的是递归版,遗憾的是没能做到尾递归,后来又琢磨出了迭代版.后来用实际编译运行测试了一遍,能正常运行. 递归版的灵感来源于<Haskell 趣学指南>中非常简洁的快速排序算法的实现,其思想是将单向链表分割头部和尾部.其中头部指是链表的第一个节点,尾部是指除去第一个节点后的子链表.通过递归的方法,将子链表继续分割成头部和尾部,直至尾部指剩下一个节点,无法继续分割,然后将头部和尾

C++模板元编程 - 3 逻辑结构,递归,一点列表的零碎,一点SFINAE

本来想把scanr,foldr什么的都写了的,一想太麻烦了,就算了,模板元编程差不多也该结束了,离开学还有10天,之前几天部门还要纳新什么的,写不了几天代码了,所以赶紧把这个结束掉,明天继续抄轮子叔的Win32库去. 逻辑结构和递归说白了就是做了一个If,一个For_N,If就和Excel里的If一样,For_N是把一个模板结构迭代N遍,为了所谓的方便,把If做成了宏,写起来还挺有意思的 1 template<typename TTest, typename TTrue, typename TF

《Haskell趣学指南 Learn You a Haskell for Great Good!》-代码实验

doubleMe x = x + x doubleUs x y = doubleMe x + doubleMe y doubleSmallNumber x = if x>100 then x else x * 2 doubleSmallNumber' x = (if x>100 then x else x * 2) + 1 boomBangs xs = [if x < 10 then "BOOM!" else "BANG!" | x <- x

Haskell 输入和输出

我们已经说明了 Haskell 是一个纯粹函数式语言.虽说在命令式语言中我们习惯给电脑执行一连串指令,在函数式语言中我们是用定义东西的方式进行.在 Haskell 中,一个函数不能改变状态,像是改变一个变量的内容.(当一个函数会改变状态,我们说这函数是有副作用的.)在 Haskell 中函数唯一可以做的事是根据我们给定的参数来算出结果.如果我们用同样的参数调用两次同一个函数,它会回传相同的结果.尽管这从命令式语言的角度来看是蛮大的限制,我们已经看过它可以达成多么酷的效果.在一个命令式语言中,编程